forked from viamrobotics/viam-python-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RSDK-4907 - Add billing client (viamrobotics#471)
- Loading branch information
Showing
6 changed files
with
292 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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): | ||
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 |