106 lines
2.3 KiB
TypeScript
106 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,
|
||
|
|
};
|
||
|
|
}
|