-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RSDK-4907 - Add billing client #471
Changes from all commits
8a22fa2
24940ea
ff1eac6
8fafa8e
36e31ed
709db5e
e03ba8a
b6ed5a0
2a2eb68
a3989f4
1989347
f2054b2
8d970c6
7f072c9
8802563
564d7dd
3d276da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
from typing import Mapping, Optional | ||
|
||
from grpclib.client import Channel | ||
|
||
from viam import logging | ||
from viam.proto.app.billing import ( | ||
BillingServiceStub, | ||
GetCurrentMonthUsageRequest, | ||
GetCurrentMonthUsageResponse, | ||
GetInvoicePdfRequest, | ||
GetInvoicePdfResponse, | ||
GetInvoicesSummaryRequest, | ||
GetInvoicesSummaryResponse, | ||
GetOrgBillingInformationRequest, | ||
GetOrgBillingInformationResponse, | ||
) | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
class BillingClient: | ||
"""gRPC client for retrieving billing data from app. | ||
|
||
Constructor is used by `ViamClient` to instantiate relevant service stubs. Calls to | ||
`BillingClient` methods should be made through `ViamClient`. | ||
""" | ||
|
||
def __init__(self, channel: Channel, metadata: Mapping[str, str]): | ||
"""Create a `BillingClient` that maintains a connection to app. | ||
|
||
Args: | ||
channel (grpclib.client.Channel): Connection to app. | ||
metadata (Mapping[str, str]): Required authorization token to send requests to app. | ||
""" | ||
self._metadata = metadata | ||
self._billing_client = BillingServiceStub(channel) | ||
self._channel = channel | ||
|
||
_billing_client: BillingServiceStub | ||
_channel: Channel | ||
_metadata: Mapping[str, str] | ||
|
||
async def get_current_month_usage(self, org_id: str, timeout: Optional[float] = None) -> GetCurrentMonthUsageResponse: | ||
"""Access data usage information for the current month for a given organization. | ||
|
||
Args: | ||
org_id (str): the ID of the organization to request usage data for | ||
|
||
Returns: | ||
viam.proto.app.billing.GetCurrentMonthUsageResponse: Current month usage information | ||
""" | ||
request = GetCurrentMonthUsageRequest(org_id=org_id) | ||
return await self._billing_client.GetCurrentMonthUsage(request, metadata=self._metadata, timeout=timeout) | ||
|
||
async def get_invoice_pdf(self, invoice_id: str, org_id: str, dest: str, timeout: Optional[float] = None) -> None: | ||
"""Access invoice PDF data and optionally save it to a provided file path. | ||
|
||
Args: | ||
invoice_id (str): the ID of the invoice being requested | ||
org_id (str): the ID of the org to request data from | ||
dest (str): filepath to save the invoice to | ||
""" | ||
request = GetInvoicePdfRequest(id=invoice_id, org_id=org_id) | ||
response: GetInvoicePdfResponse = await self._billing_client.GetInvoicePdf(request, metadata=self._metadata, timeout=timeout) | ||
data: bytes = response[0].chunk | ||
with open(dest, "wb") as file: | ||
file.write(data) | ||
|
||
async def get_invoices_summary(self, org_id: str, timeout: Optional[float] = None) -> GetInvoicesSummaryResponse: | ||
"""Access total outstanding balance plus invoice summaries for a given org. | ||
|
||
Args: | ||
org_id (str): the ID of the org to request data for | ||
|
||
Returns: | ||
viam.proto.app.billing.GetInvoicesSummaryResponse: Summary of org invoices | ||
""" | ||
request = GetInvoicesSummaryRequest(org_id=org_id) | ||
return await self._billing_client.GetInvoicesSummary(request, metadata=self._metadata, timeout=timeout) | ||
|
||
async def get_org_billing_information(self, org_id: str, timeout: Optional[float] = None) -> GetOrgBillingInformationResponse: | ||
"""Access billing information (payment method, billing tier, etc.) for a given org. | ||
|
||
Args: | ||
org_id (str): the ID of the org to request data for | ||
|
||
Returns: | ||
viam.proto.app.billing.GetOrgBillingInformationResponse: The org billing information""" | ||
request = GetOrgBillingInformationRequest(org_id=org_id) | ||
return await self._billing_client.GetOrgBillingInformation(request, metadata=self._metadata, timeout=timeout) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import pytest | ||
|
||
from google.protobuf.timestamp_pb2 import Timestamp | ||
from grpclib.testing import ChannelFor | ||
|
||
from viam.app.billing_client import BillingClient | ||
from viam.proto.app.billing import ( | ||
GetCurrentMonthUsageResponse, | ||
GetInvoicesSummaryResponse, | ||
GetOrgBillingInformationResponse, | ||
InvoiceSummary, | ||
) | ||
|
||
from .mocks.services import MockBilling | ||
|
||
PDF = b'abc123' | ||
CLOUD_STORAGE_USAGE_COST = 100.0 | ||
DATA_UPLOAD_USAGE_COST = 101.0 | ||
DATA_EGRES_USAGE_COST = 102.0 | ||
REMOTE_CONTROL_USAGE_COST = 103.0 | ||
STANDARD_COMPUTE_USAGE_COST = 104.0 | ||
DISCOUNT_AMOUNT = 0.0 | ||
TOTAL_USAGE_WITH_DISCOUNT = 105.0 | ||
TOTAL_USAGE_WITHOUT_DISCOUNT = 106.0 | ||
OUTSTANDING_BALANCE = 1000.0 | ||
SECONDS_START = 978310861 | ||
NANOS_START = 0 | ||
SECONDS_END = 998310861 | ||
NANOS_END = 0 | ||
SECONDS_PAID = 988310861 | ||
NANOS_PAID = 0 | ||
START_TS = Timestamp(seconds=SECONDS_START, nanos=NANOS_END) | ||
PAID_DATE_TS = Timestamp(seconds=SECONDS_PAID, nanos=NANOS_PAID) | ||
END_TS = Timestamp(seconds=SECONDS_END, nanos=NANOS_END) | ||
INVOICE_ID = "invoice" | ||
STATUS = "status" | ||
PAYMENT_TYPE = 1 | ||
EMAIL = "[email protected]" | ||
BILLING_TIER = "tier" | ||
INVOICE = InvoiceSummary( | ||
id=INVOICE_ID, | ||
invoice_date=START_TS, | ||
invoice_amount=OUTSTANDING_BALANCE, | ||
status=STATUS, | ||
due_date=END_TS, | ||
paid_date=PAID_DATE_TS, | ||
) | ||
INVOICES = [INVOICE] | ||
CURR_MONTH_USAGE = GetCurrentMonthUsageResponse( | ||
start_date=START_TS, | ||
end_date=END_TS, | ||
cloud_storage_usage_cost=CLOUD_STORAGE_USAGE_COST, | ||
data_upload_usage_cost=DATA_UPLOAD_USAGE_COST, | ||
data_egres_usage_cost=DATA_EGRES_USAGE_COST, | ||
remote_control_usage_cost=REMOTE_CONTROL_USAGE_COST, | ||
standard_compute_usage_cost=STANDARD_COMPUTE_USAGE_COST, | ||
discount_amount=DISCOUNT_AMOUNT, | ||
total_usage_with_discount=TOTAL_USAGE_WITH_DISCOUNT, | ||
total_usage_without_discount=TOTAL_USAGE_WITHOUT_DISCOUNT, | ||
) | ||
INVOICES_SUMMARY = GetInvoicesSummaryResponse(outstanding_balance=OUTSTANDING_BALANCE, invoices=INVOICES) | ||
ORG_BILLING_INFO = GetOrgBillingInformationResponse( | ||
type=PAYMENT_TYPE, | ||
billing_email=EMAIL, | ||
billing_tier=BILLING_TIER, | ||
) | ||
|
||
AUTH_TOKEN = "auth_token" | ||
BILLING_SERVICE_METADATA = {"authorization": f"Bearer {AUTH_TOKEN}"} | ||
|
||
|
||
@pytest.fixture(scope="function") | ||
def service() -> MockBilling: | ||
return MockBilling( | ||
pdf=PDF, | ||
curr_month_usage=CURR_MONTH_USAGE, | ||
invoices_summary=INVOICES_SUMMARY, | ||
billing_info=ORG_BILLING_INFO, | ||
) | ||
|
||
|
||
class TestClient: | ||
@pytest.mark.asyncio | ||
async def test_get_current_month_usage(self, service: MockBilling): | ||
async with ChannelFor([service]) as channel: | ||
org_id = "foo" | ||
client = BillingClient(channel, BILLING_SERVICE_METADATA) | ||
curr_month_usage = await client.get_current_month_usage(org_id=org_id) | ||
assert curr_month_usage == CURR_MONTH_USAGE | ||
assert service.org_id == org_id | ||
|
||
@pytest.mark.asyncio | ||
async def test_get_invoice_pdf(self, service: MockBilling): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to reviewers: the client code for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds reasonable to me |
||
assert True | ||
|
||
@pytest.mark.asyncio | ||
async def test_get_invoices_summary(self, service: MockBilling): | ||
async with ChannelFor([service]) as channel: | ||
org_id = "bar" | ||
client = BillingClient(channel, BILLING_SERVICE_METADATA) | ||
invoices_summary = await client.get_invoices_summary(org_id=org_id) | ||
assert invoices_summary == INVOICES_SUMMARY | ||
assert service.org_id == org_id | ||
|
||
@pytest.mark.asyncio | ||
async def test_get_org_billing_information(self, service: MockBilling): | ||
async with ChannelFor([service]) as channel: | ||
org_id = "baz" | ||
client = BillingClient(channel, BILLING_SERVICE_METADATA) | ||
org_billing_info = await client.get_org_billing_information(org_id=org_id) | ||
assert org_billing_info == ORG_BILLING_INFO | ||
assert service.org_id == org_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods are slated to be deprecated and so are not being implemented.