Skip to content
226 changes: 226 additions & 0 deletions apps/web/src/lib/kilo-pass/google-play-verifier.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { describe, expect, it, jest } from '@jest/globals';
import type { androidpublisher_v3 } from '@googleapis/androidpublisher';

import { KiloPassCadence, KiloPassPaymentProvider, KiloPassTier } from './enums';
import type * as GooglePlayVerifier from './google-play-verifier';

const mockGetGooglePlaySubscriptionPurchase =
jest.fn<(purchaseToken: string) => Promise<androidpublisher_v3.Schema$SubscriptionPurchaseV2>>();

jest.mock('./google-play-sdk', () => ({
getGooglePlaySubscriptionPurchase: mockGetGooglePlaySubscriptionPurchase,
}));

function loadVerifier(): typeof GooglePlayVerifier {
return jest.requireActual<typeof GooglePlayVerifier>('./google-play-verifier');
}

function decoded(
overrides: Partial<GooglePlayVerifier.GooglePlayDecodedPurchase> = {}
): GooglePlayVerifier.GooglePlayDecodedPurchase {
return {
purchaseToken: 'play-token-1',
productId: 'kilopass_tier19',
latestOrderId: 'GPA.1234',
startTimeMs: 1_777_626_000_000,
expiryTimeMs: 4_102_444_800_000,
obfuscatedExternalAccountId: '550e8400-e29b-41d4-a716-446655440000',
environment: 'Sandbox',
subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
rawPayload: { purchaseToken: 'play-token-1' },
...overrides,
};
}

function apiData(
overrides: Partial<androidpublisher_v3.Schema$SubscriptionPurchaseV2> = {}
): androidpublisher_v3.Schema$SubscriptionPurchaseV2 {
return {
startTime: '2026-05-01T09:00:00.000Z',
subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
externalAccountIdentifiers: {
obfuscatedExternalAccountId: '550e8400-e29b-41d4-a716-446655440000',
},
lineItems: [
{
productId: 'kilopass_tier19',
expiryTime: '2100-01-01T00:00:00.000Z',
latestSuccessfulOrderId: 'GPA.1234',
},
],
...overrides,
};
}

describe('mapGooglePlayKiloPassPurchase', () => {
it('maps a valid Play subscription purchase to a validated Kilo Pass purchase', () => {
const { mapGooglePlayKiloPassPurchase } = loadVerifier();

expect(mapGooglePlayKiloPassPurchase(decoded())).toMatchObject({
paymentProvider: KiloPassPaymentProvider.GooglePlay,
productId: 'kilopass_tier19',
providerTransactionId: 'GPA.1234',
providerOriginalTransactionId: 'play-token-1',
providerSubscriptionId: 'play-token-1',
appAccountToken: '550e8400-e29b-41d4-a716-446655440000',
purchaseToken: 'play-token-1',
expiresAtIso: '2100-01-01T00:00:00.000Z',
environment: 'Sandbox',
tier: KiloPassTier.Tier19,
cadence: KiloPassCadence.Monthly,
});
});

it('rejects an empty latest order id', () => {
const { mapGooglePlayKiloPassPurchase } = loadVerifier();

expect(() => mapGooglePlayKiloPassPurchase(decoded({ latestOrderId: '' }))).toThrow(
'Google Play purchase payload missing required identifiers'
);
});

it('rejects a non-finite start time', () => {
const { mapGooglePlayKiloPassPurchase } = loadVerifier();

expect(() => mapGooglePlayKiloPassPurchase(decoded({ startTimeMs: NaN }))).toThrow(
'Google Play subscription purchase has invalid timestamps'
);
});

it('rejects a non-finite expiry time', () => {
const { mapGooglePlayKiloPassPurchase } = loadVerifier();

expect(() => mapGooglePlayKiloPassPurchase(decoded({ expiryTimeMs: NaN }))).toThrow(
'Google Play subscription purchase has invalid timestamps'
);
});

it('rejects expired subscriptions', () => {
const { mapGooglePlayKiloPassPurchase } = loadVerifier();

expect(() =>
mapGooglePlayKiloPassPurchase(decoded({ expiryTimeMs: Date.now() - 1_000 }))
).toThrow('Google Play subscription purchase has expired');
});

it.each([
'SUBSCRIPTION_STATE_ON_HOLD',
'SUBSCRIPTION_STATE_PAUSED',
'SUBSCRIPTION_STATE_PENDING',
'SUBSCRIPTION_STATE_EXPIRED',
'SUBSCRIPTION_STATE_PENDING_PURCHASE_CANCELED',
'SUBSCRIPTION_STATE_UNSPECIFIED',
'',
])('rejects the non-entitled state %s even with a future expiry', state => {
const { mapGooglePlayKiloPassPurchase } = loadVerifier();

expect(() => mapGooglePlayKiloPassPurchase(decoded({ subscriptionState: state }))).toThrow(
'Google Play subscription purchase is not entitled'
);
});

it.each(['SUBSCRIPTION_STATE_CANCELED', 'SUBSCRIPTION_STATE_IN_GRACE_PERIOD'])(
'accepts the entitled state %s while the period is unexpired',
state => {
const { mapGooglePlayKiloPassPurchase } = loadVerifier();

expect(mapGooglePlayKiloPassPurchase(decoded({ subscriptionState: state }))).toMatchObject({
productId: 'kilopass_tier19',
tier: KiloPassTier.Tier19,
});
}
);

it('rejects unknown products', () => {
const { mapGooglePlayKiloPassPurchase } = loadVerifier();

expect(() => mapGooglePlayKiloPassPurchase(decoded({ productId: 'unknown' }))).toThrow(
'Google Play Kilo Pass product is not enabled'
);
});
});

describe('decodeGooglePlaySubscriptionPurchase', () => {
it('marks a test purchase as Sandbox', () => {
const { decodeGooglePlaySubscriptionPurchase } = loadVerifier();

const result = decodeGooglePlaySubscriptionPurchase(
apiData({ testPurchase: {} }),
'play-token-1'
);

expect(result.environment).toBe('Sandbox');
expect(result.purchaseToken).toBe('play-token-1');
expect(result.productId).toBe('kilopass_tier19');
expect(result.latestOrderId).toBe('GPA.1234');
expect(result.startTimeMs).toBe(1_777_626_000_000);
expect(result.expiryTimeMs).toBe(4_102_444_800_000);
expect(result.obfuscatedExternalAccountId).toBe('550e8400-e29b-41d4-a716-446655440000');
});

it('marks a non-test purchase as Production', () => {
const { decodeGooglePlaySubscriptionPurchase } = loadVerifier();

const result = decodeGooglePlaySubscriptionPurchase(apiData(), 'play-token-1');

expect(result.environment).toBe('Production');
});

it('throws when lineItems is empty', () => {
const { decodeGooglePlaySubscriptionPurchase } = loadVerifier();

expect(() =>
decodeGooglePlaySubscriptionPurchase(apiData({ lineItems: [] }), 'play-token-1')
).toThrow('Google Play subscription purchase missing line items');
});

it('throws when lineItems is missing', () => {
const { decodeGooglePlaySubscriptionPurchase } = loadVerifier();

expect(() =>
decodeGooglePlaySubscriptionPurchase(apiData({ lineItems: undefined }), 'play-token-1')
).toThrow('Google Play subscription purchase missing line items');
});

it('falls back to the top-level latest order id when the line item has none', () => {
const { decodeGooglePlaySubscriptionPurchase } = loadVerifier();

const data = {
startTime: '2026-05-01T09:00:00.000Z',
subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
lineItems: [
{
productId: 'kilopass_tier19',
expiryTime: '2100-01-01T00:00:00.000Z',
},
],
latestOrderId: 'GPA.fallback',
} as androidpublisher_v3.Schema$SubscriptionPurchaseV2;

const result = decodeGooglePlaySubscriptionPurchase(data, 'play-token-1');

expect(result.latestOrderId).toBe('GPA.fallback');
});
});

describe('verifyGooglePlayKiloPassPurchase', () => {
it('returns a validated purchase for the fixture payload', async () => {
const { verifyGooglePlayKiloPassPurchase } = loadVerifier();

mockGetGooglePlaySubscriptionPurchase.mockResolvedValue(apiData({ testPurchase: {} }));

const result = await verifyGooglePlayKiloPassPurchase('play-token-1');

expect(mockGetGooglePlaySubscriptionPurchase).toHaveBeenCalledWith('play-token-1');
expect(result).toMatchObject({
paymentProvider: KiloPassPaymentProvider.GooglePlay,
productId: 'kilopass_tier19',
providerTransactionId: 'GPA.1234',
providerSubscriptionId: 'play-token-1',
purchaseToken: 'play-token-1',
environment: 'Sandbox',
tier: KiloPassTier.Tier19,
cadence: KiloPassCadence.Monthly,
});
});
});
115 changes: 115 additions & 0 deletions apps/web/src/lib/kilo-pass/google-play-verifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { androidpublisher_v3 } from '@googleapis/androidpublisher';

import type { ValidatedStoreKiloPassPurchase } from './store-subscription-completion';
import { KiloPassPaymentProvider } from './enums';
import { getMobileStoreKiloPassProductByGoogleProductId } from './mobile-store-products';
import { getGooglePlaySubscriptionPurchase } from './google-play-sdk';

export type GooglePlayEnvironment = 'Sandbox' | 'Production';

export type GooglePlayDecodedPurchase = {
purchaseToken: string;
productId: string;
latestOrderId: string;
startTimeMs: number;
expiryTimeMs: number;
obfuscatedExternalAccountId?: string;
environment: GooglePlayEnvironment;
subscriptionState: string;
rawPayload: Record<string, unknown>;
};

/**
* Google Play states that entitle the buyer. `CANCELED` keeps access until the
* paid period ends, and `IN_GRACE_PERIOD` keeps access while a payment retries.
* Every other state (pending, paused, on hold, expired) grants no entitlement,
* so a purchase in one of those states must never complete as `active`.
*/
const ENTITLED_GOOGLE_PLAY_SUBSCRIPTION_STATES = new Set([
'SUBSCRIPTION_STATE_ACTIVE',
'SUBSCRIPTION_STATE_CANCELED',
'SUBSCRIPTION_STATE_IN_GRACE_PERIOD',
]);

export function mapGooglePlayKiloPassPurchase(
decoded: GooglePlayDecodedPurchase
): ValidatedStoreKiloPassPurchase {
if (!decoded.purchaseToken || !decoded.productId || !decoded.latestOrderId) {
throw new Error('Google Play purchase payload missing required identifiers');
}
if (!Number.isFinite(decoded.startTimeMs) || !Number.isFinite(decoded.expiryTimeMs)) {
throw new Error('Google Play subscription purchase has invalid timestamps');
}
// Called only from the tRPC purchase-completion path; renewals and refunds enter via
// the Play notifications handler, which intentionally allows expired purchases.
if (!ENTITLED_GOOGLE_PLAY_SUBSCRIPTION_STATES.has(decoded.subscriptionState)) {
throw new Error(
`Google Play subscription purchase is not entitled: ${decoded.subscriptionState || 'unknown'}`
);
}
if (decoded.expiryTimeMs <= Date.now()) {
Comment thread
iscekic marked this conversation as resolved.
throw new Error('Google Play subscription purchase has expired');
}

const product = getMobileStoreKiloPassProductByGoogleProductId(decoded.productId);
if (!product) {
throw new Error('Google Play Kilo Pass product is not enabled');
}

return {
paymentProvider: KiloPassPaymentProvider.GooglePlay,
productId: decoded.productId,
providerTransactionId: decoded.latestOrderId,
providerOriginalTransactionId: decoded.purchaseToken,
providerSubscriptionId: decoded.purchaseToken,
appAccountToken: decoded.obfuscatedExternalAccountId ?? null,
purchaseToken: decoded.purchaseToken,
environment: decoded.environment,
purchasedAtIso: new Date(decoded.startTimeMs).toISOString(),
expiresAtIso: new Date(decoded.expiryTimeMs).toISOString(),
tier: product.tier,
cadence: product.cadence,
rawPayload: decoded.rawPayload,
};
}

export function decodeGooglePlaySubscriptionPurchase(
apiData: androidpublisher_v3.Schema$SubscriptionPurchaseV2,
purchaseToken: string
): GooglePlayDecodedPurchase {
const lineItems = apiData.lineItems ?? [];
if (lineItems.length === 0) {
throw new Error('Google Play subscription purchase missing line items');
}
const lineItem = lineItems[0];

const latestOrderId =
lineItem.latestSuccessfulOrderId ??
(apiData as { latestOrderId?: string | null }).latestOrderId ??
'';
if (!latestOrderId) {
throw new Error('Google Play purchase payload missing required identifiers');
}

return {
purchaseToken,
productId: lineItem.productId ?? '',
latestOrderId,
startTimeMs: Date.parse(apiData.startTime ?? ''),
Comment thread
iscekic marked this conversation as resolved.
expiryTimeMs: Date.parse(lineItem.expiryTime ?? ''),
obfuscatedExternalAccountId:
apiData.externalAccountIdentifiers?.obfuscatedExternalAccountId ?? undefined,
environment: apiData.testPurchase != null ? 'Sandbox' : 'Production',
subscriptionState: apiData.subscriptionState ?? '',
rawPayload: apiData as unknown as Record<string, unknown>,
};
}

export async function verifyGooglePlayKiloPassPurchase(
purchaseToken: string
): Promise<ValidatedStoreKiloPassPurchase> {
const apiData = await getGooglePlaySubscriptionPurchase(purchaseToken);
return mapGooglePlayKiloPassPurchase(
decodeGooglePlaySubscriptionPurchase(apiData, purchaseToken)
);
}