import {ConfigInterface} from '../base-types'; import {BillingError, GraphqlQueryError} from '../error'; import {GraphqlClient, graphqlClientClass} from '../clients/admin'; import { AppSubscription, BillingCreateUsageRecord, BillingCreateUsageRecordParams, UsageRecord, UsageRecordCreateResponse, Money, } from './types'; import {assessPayments} from './check'; import { convertAppRecurringPricingMoney, convertAppUsagePricingMoney, } from './utils'; interface InternalParams { client: GraphqlClient; isTest?: boolean; } interface CreateUsageRecordVariables { description: string; price: Money; subscriptionLineItemId: string; idempotencyKey?: string; } const CREATE_USAGE_RECORD_MUTATION = ` mutation appUsageRecordCreate($description: String!, $price: MoneyInput!, $subscriptionLineItemId: ID!) { appUsageRecordCreate(description: $description, price: $price, subscriptionLineItemId: $subscriptionLineItemId) { userErrors { field message } appUsageRecord { id description idempotencyKey price { amount currencyCode } subscriptionLineItem { id plan { pricingDetails { ... on AppUsagePricing { balanceUsed { amount currencyCode } cappedAmount { amount currencyCode } terms } } } } } } } `; export function createUsageRecord( config: ConfigInterface, ): BillingCreateUsageRecord { return async function createUsageRecord( usageRecordInfo: BillingCreateUsageRecordParams, ): Promise { const { session, subscriptionLineItemId, description, price, idempotencyKey, isTest = true, } = usageRecordInfo; const GraphqlClient = graphqlClientClass({config}); const client = new GraphqlClient({session}); // If a subscription line item ID is not passed, we will query Shopify // for an active usage subscription line item ID const usageSubscriptionLineItemId = subscriptionLineItemId ? subscriptionLineItemId : await getUsageRecordSubscriptionLineItemId({client, isTest}); const variables: CreateUsageRecordVariables = { description, price, subscriptionLineItemId: usageSubscriptionLineItemId, }; if (idempotencyKey) { variables.idempotencyKey = idempotencyKey; } try { const response = await client.request( CREATE_USAGE_RECORD_MUTATION, { variables, }, ); if (response.data?.appUsageRecordCreate?.userErrors.length) { throw new BillingError({ message: 'Error while creating a usage record', errorData: response.data?.appUsageRecordCreate?.userErrors, }); } const appUsageRecord = response.data?.appUsageRecordCreate?.appUsageRecord!; convertAppRecurringPricingMoney(appUsageRecord.price); convertAppUsagePricingMoney( appUsageRecord.subscriptionLineItem.plan.pricingDetails, ); return appUsageRecord; } catch (error) { if (error instanceof GraphqlQueryError) { throw new BillingError({ message: error.message, errorData: error.response?.errors, }); } else { throw error; } } }; } async function getUsageRecordSubscriptionLineItemId({ client, isTest, }: InternalParams): Promise { const payments = await assessPayments({client, isTest}); if (!payments.hasActivePayment) { throw new BillingError({ message: 'No active payment found', errorData: [], }); } if (!payments.appSubscriptions.length) { throw new BillingError({ message: 'No active subscriptions found', errorData: [], }); } if (payments.appSubscriptions) { const usageSubscriptionLineItemId = getUsageLineItemId( payments.appSubscriptions, ); return usageSubscriptionLineItemId; } throw new BillingError({ message: 'Unable to find active subscription line item', errorData: [], }); } function getUsageLineItemId(subscriptions: AppSubscription[]): string { for (const subscription of subscriptions) { // An app can have only one active subscription if (subscription.status === 'ACTIVE' && subscription.lineItems) { // An app can have only one usage subscription line item for (const lineItem of subscription.lineItems) { if ('balanceUsed' in lineItem.plan.pricingDetails) { return lineItem.id; } } } } throw new BillingError({ message: 'No active usage subscription found', errorData: [], }); }