Files
cfw-autumn/server/src/internal/balances/autoTopUp/webhooks/sendAutoTopupSucceededWebhook.ts

133 lines
3.2 KiB
TypeScript

import {
ACTIVE_STATUSES,
type BillingAutoTopupSucceededInvoice,
type BillingResult,
fullCustomerToCustomerEntitlements,
getApiBalance,
WebhookEventType,
} from "@autumn/shared";
import { sendSvixEvent } from "@/external/svix/svixHelpers.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { CusService } from "@/internal/customers/CusService.js";
import { generateId } from "@/utils/genUtils.js";
import type { AutoTopupContext } from "../autoTopupContext.js";
const getInvoicePayload = ({
billingResult,
}: {
billingResult: BillingResult;
}): BillingAutoTopupSucceededInvoice | null => {
const stripeInvoice = billingResult.stripe.stripeInvoice;
if (!stripeInvoice) return null;
return {
stripe_id: stripeInvoice.id,
status: stripeInvoice.status,
total: stripeInvoice.total,
currency: stripeInvoice.currency,
hosted_invoice_url: stripeInvoice.hosted_invoice_url,
};
};
// Refetches because executeBillingPlan applied the rebalance via SQL increments;
// the in-memory autoTopupContext.customerEntitlement is now stale.
const getBalanceAfter = async ({
ctx,
autoTopupContext,
}: {
ctx: AutumnContext;
autoTopupContext: AutoTopupContext;
}): Promise<number> => {
const feature = autoTopupContext.customerEntitlement.entitlement.feature;
const customerId =
autoTopupContext.fullCustomer.id ??
autoTopupContext.fullCustomer.internal_id;
const fullCustomer = await CusService.getFull({
ctx,
idOrInternalId: customerId,
inStatuses: ACTIVE_STATUSES,
withSubs: true,
});
const customerEntitlements = fullCustomerToCustomerEntitlements({
fullCustomer,
featureId: feature.id,
});
const { data: balance } = getApiBalance({
ctx,
fullCus: fullCustomer,
cusEnts: customerEntitlements,
feature,
});
return balance.remaining;
};
const sendAutoTopupSucceededWebhookUnsafe = async ({
ctx,
autoTopupContext,
billingResult,
}: {
ctx: AutumnContext;
autoTopupContext: AutoTopupContext;
billingResult: BillingResult;
}) => {
const invoice = getInvoicePayload({
billingResult,
});
if (!invoice) {
ctx.logger.warn(
"[sendAutoTopupSucceededWebhook] Missing invoice, skipping webhook",
);
return;
}
const customerId =
autoTopupContext.fullCustomer.id ??
autoTopupContext.fullCustomer.internal_id;
const balanceAfter = await getBalanceAfter({
ctx,
autoTopupContext,
});
await sendSvixEvent({
ctx,
eventType: WebhookEventType.BillingAutoTopupSucceeded,
payloadFields: {
id: generateId("evt_auto_topup"),
occurred_at: Date.now(),
},
data: {
customer_id: customerId,
feature_id: autoTopupContext.autoTopupConfig.feature_id,
quantity_granted: autoTopupContext.autoTopupConfig.quantity,
threshold: autoTopupContext.autoTopupConfig.threshold,
balance_after: balanceAfter,
invoice_mode: Boolean(autoTopupContext.invoiceMode),
invoice,
},
});
};
export const sendAutoTopupSucceededWebhook = async ({
ctx,
autoTopupContext,
billingResult,
}: {
ctx: AutumnContext;
autoTopupContext: AutoTopupContext;
billingResult: BillingResult;
}) => {
try {
await sendAutoTopupSucceededWebhookUnsafe({
ctx,
autoTopupContext,
billingResult,
});
} catch (error) {
ctx.logger.error(
`[sendAutoTopupSucceededWebhook] Failed to send webhook: ${error}`,
{ error },
);
}
};