xtablo-source/apps/api/src/helpers/appleBilling.ts
2026-05-03 09:28:46 +02:00

105 lines
2.3 KiB
TypeScript

export type AppleBillingStatus =
| "active"
| "expired"
| "canceled"
| "refunded"
| "revoked"
| "unknown";
export type AppleBillingCandidate = {
currentPeriodEnd: number;
plan: "solo" | "annual";
quantity: 1;
status: AppleBillingStatus;
};
export const APPLE_ACCESSIBLE_STATUSES: AppleBillingStatus[] = ["active", "canceled"];
export function mapAppleProductToPlan(
productId: string | null | undefined,
config: {
annualProductId: string;
soloProductId: string;
}
): "solo" | "annual" | null {
if (!productId) {
return null;
}
if (productId === config.soloProductId) {
return "solo";
}
if (productId === config.annualProductId) {
return "annual";
}
return null;
}
export function normalizeAppleSubscriptionStatus(
eventType: string | null | undefined,
cancelAtPeriodEnd: boolean
): AppleBillingStatus {
const normalizedEventType = (eventType ?? "").trim().toUpperCase();
if (normalizedEventType === "EXPIRATION") {
return "expired";
}
if (normalizedEventType === "CANCELLATION") {
return cancelAtPeriodEnd ? "canceled" : "revoked";
}
if (normalizedEventType === "BILLING_ISSUE") {
return "canceled";
}
if (normalizedEventType === "SUBSCRIPTION_PAUSED") {
return "canceled";
}
if (normalizedEventType === "UNCANCELLATION") {
return "active";
}
if (
normalizedEventType === "INITIAL_PURCHASE" ||
normalizedEventType === "NON_RENEWING_PURCHASE" ||
normalizedEventType === "PRODUCT_CHANGE" ||
normalizedEventType === "RENEWAL" ||
normalizedEventType === "TRANSFER"
) {
return "active";
}
return "unknown";
}
export function toAppleBillingCandidate(input: {
currentPeriodEnd: string | null;
now?: Date;
plan: "solo" | "annual";
status: AppleBillingStatus;
}): AppleBillingCandidate | null {
if (!APPLE_ACCESSIBLE_STATUSES.includes(input.status)) {
return null;
}
const currentPeriodEndMs = input.currentPeriodEnd ? Date.parse(input.currentPeriodEnd) : NaN;
if (Number.isNaN(currentPeriodEndMs)) {
return null;
}
const now = input.now ?? new Date();
if (currentPeriodEndMs <= now.getTime()) {
return null;
}
return {
currentPeriodEnd: Math.floor(currentPeriodEndMs / 1000),
plan: input.plan,
quantity: 1,
status: input.status,
};
}