@onesub/server 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +1 -0
  2. package/dist/__tests__/apple-status-api.test.d.ts +14 -0
  3. package/dist/__tests__/apple-status-api.test.d.ts.map +1 -0
  4. package/dist/__tests__/apple-status-api.test.js +345 -0
  5. package/dist/__tests__/apple-status-api.test.js.map +1 -0
  6. package/dist/__tests__/error-codes.test.js +16 -0
  7. package/dist/__tests__/error-codes.test.js.map +1 -1
  8. package/dist/__tests__/google-paused.test.d.ts +10 -0
  9. package/dist/__tests__/google-paused.test.d.ts.map +1 -0
  10. package/dist/__tests__/google-paused.test.js +126 -0
  11. package/dist/__tests__/google-paused.test.js.map +1 -0
  12. package/dist/__tests__/google-price-change.test.d.ts +6 -0
  13. package/dist/__tests__/google-price-change.test.d.ts.map +1 -0
  14. package/dist/__tests__/google-price-change.test.js +186 -0
  15. package/dist/__tests__/google-price-change.test.js.map +1 -0
  16. package/dist/__tests__/google-subscriptions-v2.test.d.ts +13 -0
  17. package/dist/__tests__/google-subscriptions-v2.test.d.ts.map +1 -0
  18. package/dist/__tests__/google-subscriptions-v2.test.js +206 -0
  19. package/dist/__tests__/google-subscriptions-v2.test.js.map +1 -0
  20. package/dist/__tests__/lifecycle-states.test.d.ts +12 -0
  21. package/dist/__tests__/lifecycle-states.test.d.ts.map +1 -0
  22. package/dist/__tests__/lifecycle-states.test.js +423 -0
  23. package/dist/__tests__/lifecycle-states.test.js.map +1 -0
  24. package/dist/__tests__/linked-purchase-token.test.d.ts +9 -0
  25. package/dist/__tests__/linked-purchase-token.test.d.ts.map +1 -0
  26. package/dist/__tests__/linked-purchase-token.test.js +227 -0
  27. package/dist/__tests__/linked-purchase-token.test.js.map +1 -0
  28. package/dist/__tests__/mock-provider.test.d.ts +2 -0
  29. package/dist/__tests__/mock-provider.test.d.ts.map +1 -0
  30. package/dist/__tests__/mock-provider.test.js +188 -0
  31. package/dist/__tests__/mock-provider.test.js.map +1 -0
  32. package/dist/__tests__/providers.test.js +69 -9
  33. package/dist/__tests__/providers.test.js.map +1 -1
  34. package/dist/__tests__/refund-policy.test.d.ts +11 -0
  35. package/dist/__tests__/refund-policy.test.d.ts.map +1 -0
  36. package/dist/__tests__/refund-policy.test.js +317 -0
  37. package/dist/__tests__/refund-policy.test.js.map +1 -0
  38. package/dist/__tests__/store.test.js +17 -0
  39. package/dist/__tests__/store.test.js.map +1 -1
  40. package/dist/__tests__/test-utils.d.ts +20 -0
  41. package/dist/__tests__/test-utils.d.ts.map +1 -0
  42. package/dist/__tests__/test-utils.js +30 -0
  43. package/dist/__tests__/test-utils.js.map +1 -0
  44. package/dist/__tests__/webhook-refund.test.d.ts +13 -0
  45. package/dist/__tests__/webhook-refund.test.d.ts.map +1 -0
  46. package/dist/__tests__/webhook-refund.test.js +313 -0
  47. package/dist/__tests__/webhook-refund.test.js.map +1 -0
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +9 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/providers/apple.d.ts +45 -2
  53. package/dist/providers/apple.d.ts.map +1 -1
  54. package/dist/providers/apple.js +202 -2
  55. package/dist/providers/apple.js.map +1 -1
  56. package/dist/providers/google.d.ts +71 -6
  57. package/dist/providers/google.d.ts.map +1 -1
  58. package/dist/providers/google.js +204 -32
  59. package/dist/providers/google.js.map +1 -1
  60. package/dist/providers/mock.d.ts +46 -0
  61. package/dist/providers/mock.d.ts.map +1 -0
  62. package/dist/providers/mock.js +93 -0
  63. package/dist/providers/mock.js.map +1 -0
  64. package/dist/routes/admin.d.ts.map +1 -1
  65. package/dist/routes/admin.js +4 -2
  66. package/dist/routes/admin.js.map +1 -1
  67. package/dist/routes/purchase.d.ts.map +1 -1
  68. package/dist/routes/purchase.js +15 -6
  69. package/dist/routes/purchase.js.map +1 -1
  70. package/dist/routes/status.d.ts.map +1 -1
  71. package/dist/routes/status.js +14 -1
  72. package/dist/routes/status.js.map +1 -1
  73. package/dist/routes/validate.d.ts.map +1 -1
  74. package/dist/routes/validate.js +7 -1
  75. package/dist/routes/validate.js.map +1 -1
  76. package/dist/routes/webhook.d.ts +2 -2
  77. package/dist/routes/webhook.d.ts.map +1 -1
  78. package/dist/routes/webhook.js +219 -27
  79. package/dist/routes/webhook.js.map +1 -1
  80. package/dist/store.d.ts +9 -0
  81. package/dist/store.d.ts.map +1 -1
  82. package/dist/store.js +12 -0
  83. package/dist/store.js.map +1 -1
  84. package/dist/stores/postgres.d.ts +1 -0
  85. package/dist/stores/postgres.d.ts.map +1 -1
  86. package/dist/stores/postgres.js +10 -2
  87. package/dist/stores/postgres.js.map +1 -1
  88. package/dist/stores/schema.d.ts.map +1 -1
  89. package/dist/stores/schema.js +5 -0
  90. package/dist/stores/schema.js.map +1 -1
  91. package/package.json +2 -2
  92. package/sql/schema.sql +5 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Tests for Google webhook SUBSCRIPTION_PRICE_CHANGE_CONFIRMED (8) handling
3
+ * + the onPriceChangeConfirmed hook.
4
+ */
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import express from 'express';
7
+ import { SUBSCRIPTION_STATUS } from '@onesub/shared';
8
+ import { createWebhookRouter } from '../routes/webhook.js';
9
+ import { InMemorySubscriptionStore, InMemoryPurchaseStore } from '../store.js';
10
+ function spinUp(handler) {
11
+ return {
12
+ async request(path, body) {
13
+ const server = handler.listen(0);
14
+ const address = server.address();
15
+ const port = typeof address === 'object' && address ? address.port : 0;
16
+ try {
17
+ const resp = await fetch(`http://127.0.0.1:${port}${path}`, {
18
+ method: 'POST',
19
+ headers: { 'Content-Type': 'application/json' },
20
+ body: JSON.stringify(body),
21
+ });
22
+ const text = await resp.text();
23
+ let parsed = text;
24
+ try {
25
+ parsed = JSON.parse(text);
26
+ }
27
+ catch { /* keep as text */ }
28
+ return { status: resp.status, body: parsed };
29
+ }
30
+ finally {
31
+ await new Promise((resolve) => server.close(() => resolve()));
32
+ }
33
+ },
34
+ };
35
+ }
36
+ const sampleSub = (overrides) => ({
37
+ userId: 'user_pc',
38
+ productId: 'pro_monthly',
39
+ platform: 'google',
40
+ status: 'active',
41
+ expiresAt: '2099-01-01T00:00:00.000Z',
42
+ originalTransactionId: 'tok_pc',
43
+ purchasedAt: '2026-01-01T00:00:00.000Z',
44
+ willRenew: true,
45
+ ...overrides,
46
+ });
47
+ function priceChangePushBody(purchaseToken = 'tok_pc') {
48
+ const json = JSON.stringify({
49
+ version: '1.0',
50
+ packageName: 'com.example.app',
51
+ eventTimeMillis: String(Date.now()),
52
+ subscriptionNotification: {
53
+ version: '1.0',
54
+ notificationType: 8, // SUBSCRIPTION_PRICE_CHANGE_CONFIRMED
55
+ purchaseToken,
56
+ subscriptionId: 'pro_monthly',
57
+ },
58
+ });
59
+ return {
60
+ message: { data: Buffer.from(json).toString('base64'), messageId: '1' },
61
+ subscription: 's',
62
+ };
63
+ }
64
+ function buildServer(config) {
65
+ const store = new InMemorySubscriptionStore();
66
+ const purchaseStore = new InMemoryPurchaseStore();
67
+ const app = express();
68
+ app.use(express.json());
69
+ app.use(createWebhookRouter(config, store, purchaseStore));
70
+ return { store, server: spinUp(app) };
71
+ }
72
+ describe('Google webhook — SUBSCRIPTION_PRICE_CHANGE_CONFIRMED (8)', () => {
73
+ it('keeps status=active for the existing record', async () => {
74
+ const { store, server } = buildServer({
75
+ google: { packageName: 'com.example.app' },
76
+ database: { url: '' },
77
+ });
78
+ await store.save(sampleSub({ originalTransactionId: 'tok_keep' }));
79
+ const resp = await server.request('/onesub/webhook/google', priceChangePushBody('tok_keep'));
80
+ expect(resp.status).toBe(200);
81
+ expect((await store.getByTransactionId('tok_keep'))?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
82
+ });
83
+ it('invokes onPriceChangeConfirmed hook with correct context', async () => {
84
+ const captured = [];
85
+ const { store, server } = buildServer({
86
+ google: {
87
+ packageName: 'com.example.app',
88
+ onPriceChangeConfirmed: (ctx) => {
89
+ captured.push(ctx);
90
+ },
91
+ },
92
+ database: { url: '' },
93
+ });
94
+ await store.save(sampleSub({ originalTransactionId: 'tok_hook' }));
95
+ await server.request('/onesub/webhook/google', priceChangePushBody('tok_hook'));
96
+ // Hook is fire-and-forget — give microtask queue a tick
97
+ await new Promise((r) => setTimeout(r, 30));
98
+ expect(captured).toHaveLength(1);
99
+ expect(captured[0]).toEqual({
100
+ purchaseToken: 'tok_hook',
101
+ subscriptionId: 'pro_monthly',
102
+ packageName: 'com.example.app',
103
+ });
104
+ });
105
+ it('async hook is awaited internally without blocking the webhook response', async () => {
106
+ let hookResolved = false;
107
+ const { store, server } = buildServer({
108
+ google: {
109
+ packageName: 'com.example.app',
110
+ onPriceChangeConfirmed: async () => {
111
+ await new Promise((r) => setTimeout(r, 50));
112
+ hookResolved = true;
113
+ },
114
+ },
115
+ database: { url: '' },
116
+ });
117
+ await store.save(sampleSub({ originalTransactionId: 'tok_async' }));
118
+ const start = Date.now();
119
+ const resp = await server.request('/onesub/webhook/google', priceChangePushBody('tok_async'));
120
+ const responseLatency = Date.now() - start;
121
+ // Webhook returned 200 fast — host's slow hook didn't block
122
+ expect(resp.status).toBe(200);
123
+ expect(responseLatency).toBeLessThan(50);
124
+ expect(hookResolved).toBe(false);
125
+ // Hook still completes eventually
126
+ await new Promise((r) => setTimeout(r, 80));
127
+ expect(hookResolved).toBe(true);
128
+ });
129
+ it('hook errors are swallowed (webhook still returns 200)', async () => {
130
+ const { store, server } = buildServer({
131
+ google: {
132
+ packageName: 'com.example.app',
133
+ onPriceChangeConfirmed: () => {
134
+ throw new Error('boom');
135
+ },
136
+ },
137
+ database: { url: '' },
138
+ });
139
+ await store.save(sampleSub({ originalTransactionId: 'tok_throw' }));
140
+ const resp = await server.request('/onesub/webhook/google', priceChangePushBody('tok_throw'));
141
+ expect(resp.status).toBe(200);
142
+ });
143
+ it('does nothing extra when hook is not configured', async () => {
144
+ const { store, server } = buildServer({
145
+ google: { packageName: 'com.example.app' },
146
+ database: { url: '' },
147
+ });
148
+ await store.save(sampleSub({ originalTransactionId: 'tok_nohook' }));
149
+ const resp = await server.request('/onesub/webhook/google', priceChangePushBody('tok_nohook'));
150
+ expect(resp.status).toBe(200);
151
+ // Subscription still active — webhook didn't fail just because no hook
152
+ expect((await store.getByTransactionId('tok_nohook'))?.status).toBe(SUBSCRIPTION_STATUS.ACTIVE);
153
+ });
154
+ it('hook is NOT invoked for other notification types (e.g. DID_RENEW)', async () => {
155
+ const hookSpy = vi.fn();
156
+ const { store, server } = buildServer({
157
+ google: {
158
+ packageName: 'com.example.app',
159
+ onPriceChangeConfirmed: hookSpy,
160
+ },
161
+ database: { url: '' },
162
+ });
163
+ await store.save(sampleSub({ originalTransactionId: 'tok_other' }));
164
+ const renewBody = {
165
+ message: {
166
+ data: Buffer.from(JSON.stringify({
167
+ version: '1.0',
168
+ packageName: 'com.example.app',
169
+ eventTimeMillis: String(Date.now()),
170
+ subscriptionNotification: {
171
+ version: '1.0',
172
+ notificationType: 2, // SUBSCRIPTION_RENEWED
173
+ purchaseToken: 'tok_other',
174
+ subscriptionId: 'pro_monthly',
175
+ },
176
+ })).toString('base64'),
177
+ messageId: '1',
178
+ },
179
+ subscription: 's',
180
+ };
181
+ await server.request('/onesub/webhook/google', renewBody);
182
+ await new Promise((r) => setTimeout(r, 30));
183
+ expect(hookSpy).not.toHaveBeenCalled();
184
+ });
185
+ });
186
+ //# sourceMappingURL=google-price-change.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-price-change.test.js","sourceRoot":"","sources":["../../src/__tests__/google-price-change.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Tests for the validateGoogleReceipt v1 → v2 migration.
3
+ *
4
+ * Covers:
5
+ * - Calls purchases.subscriptionsv2 endpoint (no productId in URL)
6
+ * - subscriptionState string enum → onesub SubscriptionStatus mapping
7
+ * - lineItems productId match (success / mismatch)
8
+ * - autoRenewingPlan.autoRenewEnabled → willRenew
9
+ * - latestOrderId → originalTransactionId
10
+ * - PENDING / UNSPECIFIED states rejected (return null)
11
+ */
12
+ export {};
13
+ //# sourceMappingURL=google-subscriptions-v2.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-subscriptions-v2.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/google-subscriptions-v2.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Tests for the validateGoogleReceipt v1 → v2 migration.
3
+ *
4
+ * Covers:
5
+ * - Calls purchases.subscriptionsv2 endpoint (no productId in URL)
6
+ * - subscriptionState string enum → onesub SubscriptionStatus mapping
7
+ * - lineItems productId match (success / mismatch)
8
+ * - autoRenewingPlan.autoRenewEnabled → willRenew
9
+ * - latestOrderId → originalTransactionId
10
+ * - PENDING / UNSPECIFIED states rejected (return null)
11
+ */
12
+ import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
13
+ import { generateKeyPairSync } from 'crypto';
14
+ import { validateGoogleReceipt } from '../providers/google.js';
15
+ import { SUBSCRIPTION_STATUS } from '@onesub/shared';
16
+ import { urlHost } from './test-utils.js';
17
+ // ── helpers ─────────────────────────────────────────────────────────────────
18
+ let testPrivateKey;
19
+ beforeAll(() => {
20
+ const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
21
+ testPrivateKey = privateKey.export({ type: 'pkcs8', format: 'pem' });
22
+ });
23
+ beforeEach(() => {
24
+ vi.restoreAllMocks();
25
+ });
26
+ function makeGoogleConfig() {
27
+ return {
28
+ packageName: 'com.example.app',
29
+ serviceAccountKey: JSON.stringify({
30
+ client_email: `test-${Math.random()}@test.iam.gserviceaccount.com`,
31
+ private_key: testPrivateKey,
32
+ token_uri: 'https://oauth2.googleapis.com/token',
33
+ }),
34
+ };
35
+ }
36
+ function mockV2Fetch(responseBody, opts) {
37
+ const calls = [];
38
+ vi.spyOn(global, 'fetch').mockImplementation(async (url) => {
39
+ if (urlHost(url) === 'oauth2.googleapis.com') {
40
+ return {
41
+ ok: true,
42
+ json: async () => ({ access_token: 'tok', expires_in: 3600 }),
43
+ text: async () => '',
44
+ };
45
+ }
46
+ calls.push({ url: String(url) });
47
+ return {
48
+ ok: (opts?.status ?? 200) < 400,
49
+ status: opts?.status ?? 200,
50
+ json: async () => responseBody,
51
+ text: async () => JSON.stringify(responseBody),
52
+ };
53
+ });
54
+ return calls;
55
+ }
56
+ function v2Active(productId = 'pro_monthly') {
57
+ return {
58
+ startTime: '2026-01-01T00:00:00Z',
59
+ subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
60
+ latestOrderId: 'GPA.1234-5678-9012-12345',
61
+ acknowledgementState: 'ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED',
62
+ lineItems: [{
63
+ productId,
64
+ expiryTime: '2027-01-01T00:00:00Z',
65
+ autoRenewingPlan: { autoRenewEnabled: true },
66
+ }],
67
+ };
68
+ }
69
+ // ── URL shape ───────────────────────────────────────────────────────────────
70
+ describe('validateGoogleReceipt — v2 endpoint', () => {
71
+ it('calls purchases.subscriptionsv2 (token only, no productId in URL)', async () => {
72
+ const calls = mockV2Fetch(v2Active('pro_monthly'));
73
+ await validateGoogleReceipt('purchase_token_xyz', 'pro_monthly', makeGoogleConfig());
74
+ const apiCall = calls.find((c) => urlHost(c.url) === 'androidpublisher.googleapis.com');
75
+ expect(apiCall).toBeDefined();
76
+ expect(apiCall?.url).toContain('/purchases/subscriptionsv2/tokens/purchase_token_xyz');
77
+ expect(apiCall?.url).not.toContain('/purchases/subscriptions/');
78
+ });
79
+ });
80
+ // ── subscriptionState mapping ───────────────────────────────────────────────
81
+ describe('validateGoogleReceipt — subscriptionState mapping', () => {
82
+ const cases = [
83
+ ['SUBSCRIPTION_STATE_ACTIVE', SUBSCRIPTION_STATUS.ACTIVE],
84
+ ['SUBSCRIPTION_STATE_IN_GRACE_PERIOD', SUBSCRIPTION_STATUS.GRACE_PERIOD],
85
+ ['SUBSCRIPTION_STATE_ON_HOLD', SUBSCRIPTION_STATUS.ON_HOLD],
86
+ ['SUBSCRIPTION_STATE_PAUSED', SUBSCRIPTION_STATUS.PAUSED],
87
+ ['SUBSCRIPTION_STATE_CANCELED', SUBSCRIPTION_STATUS.CANCELED],
88
+ ['SUBSCRIPTION_STATE_EXPIRED', SUBSCRIPTION_STATUS.EXPIRED],
89
+ ];
90
+ for (const [state, expected] of cases) {
91
+ it(`${state} → ${expected}`, async () => {
92
+ mockV2Fetch({ ...v2Active('pro_monthly'), subscriptionState: state });
93
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
94
+ expect(result?.status).toBe(expected);
95
+ });
96
+ }
97
+ it('SUBSCRIPTION_STATE_PENDING → null (entitlement not yet granted)', async () => {
98
+ mockV2Fetch({ ...v2Active('pro_monthly'), subscriptionState: 'SUBSCRIPTION_STATE_PENDING' });
99
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
100
+ expect(result).toBeNull();
101
+ });
102
+ it('SUBSCRIPTION_STATE_UNSPECIFIED → null', async () => {
103
+ mockV2Fetch({ ...v2Active('pro_monthly'), subscriptionState: 'SUBSCRIPTION_STATE_UNSPECIFIED' });
104
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
105
+ expect(result).toBeNull();
106
+ });
107
+ it('unknown state string → null', async () => {
108
+ mockV2Fetch({ ...v2Active('pro_monthly'), subscriptionState: 'SOMETHING_NEW_FROM_GOOGLE' });
109
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
110
+ expect(result).toBeNull();
111
+ });
112
+ });
113
+ // ── lineItems productId matching ────────────────────────────────────────────
114
+ describe('validateGoogleReceipt — lineItems productId match', () => {
115
+ it('returns null when no lineItem matches the requested productId', async () => {
116
+ mockV2Fetch(v2Active('different_product'));
117
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
118
+ expect(result).toBeNull();
119
+ });
120
+ it('returns null when lineItems is empty', async () => {
121
+ mockV2Fetch({ ...v2Active(), lineItems: [] });
122
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
123
+ expect(result).toBeNull();
124
+ });
125
+ it('picks the matching lineItem when multiple are present (multi-product)', async () => {
126
+ mockV2Fetch({
127
+ ...v2Active(),
128
+ lineItems: [
129
+ { productId: 'addon_a', expiryTime: '2099-01-01T00:00:00Z', autoRenewingPlan: { autoRenewEnabled: true } },
130
+ { productId: 'pro_monthly', expiryTime: '2027-06-01T00:00:00Z', autoRenewingPlan: { autoRenewEnabled: true } },
131
+ { productId: 'addon_b', expiryTime: '2099-12-31T00:00:00Z', autoRenewingPlan: { autoRenewEnabled: false } },
132
+ ],
133
+ });
134
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
135
+ expect(result).not.toBeNull();
136
+ expect(result?.productId).toBe('pro_monthly');
137
+ expect(result?.expiresAt).toBe('2027-06-01T00:00:00Z');
138
+ expect(result?.willRenew).toBe(true);
139
+ });
140
+ });
141
+ // ── field mapping ───────────────────────────────────────────────────────────
142
+ describe('validateGoogleReceipt — field mapping', () => {
143
+ it('uses latestOrderId as originalTransactionId', async () => {
144
+ mockV2Fetch({ ...v2Active('pro_monthly'), latestOrderId: 'GPA.canonical-order-id' });
145
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
146
+ expect(result?.originalTransactionId).toBe('GPA.canonical-order-id');
147
+ });
148
+ it('falls back to truncated purchaseToken when latestOrderId is absent', async () => {
149
+ const noOrder = v2Active('pro_monthly');
150
+ delete noOrder.latestOrderId;
151
+ mockV2Fetch(noOrder);
152
+ const result = await validateGoogleReceipt('a-very-very-long-token-' + 'x'.repeat(100), 'pro_monthly', makeGoogleConfig());
153
+ expect(result?.originalTransactionId).toBeDefined();
154
+ expect(result?.originalTransactionId.length).toBeLessThanOrEqual(64);
155
+ });
156
+ it('autoRenewEnabled false → willRenew false', async () => {
157
+ mockV2Fetch({
158
+ ...v2Active(),
159
+ lineItems: [{
160
+ productId: 'pro_monthly',
161
+ expiryTime: '2027-01-01T00:00:00Z',
162
+ autoRenewingPlan: { autoRenewEnabled: false },
163
+ }],
164
+ });
165
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
166
+ expect(result?.willRenew).toBe(false);
167
+ });
168
+ it('missing autoRenewingPlan → willRenew false', async () => {
169
+ mockV2Fetch({
170
+ ...v2Active(),
171
+ lineItems: [{ productId: 'pro_monthly', expiryTime: '2027-01-01T00:00:00Z' }],
172
+ });
173
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
174
+ expect(result?.willRenew).toBe(false);
175
+ });
176
+ it('uses lineItem.expiryTime for expiresAt and startTime for purchasedAt', async () => {
177
+ mockV2Fetch({
178
+ startTime: '2025-12-01T10:00:00Z',
179
+ subscriptionState: 'SUBSCRIPTION_STATE_ACTIVE',
180
+ latestOrderId: 'GPA.x',
181
+ lineItems: [{
182
+ productId: 'pro_monthly',
183
+ expiryTime: '2026-12-01T10:00:00Z',
184
+ autoRenewingPlan: { autoRenewEnabled: true },
185
+ }],
186
+ });
187
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
188
+ expect(result?.purchasedAt).toBe('2025-12-01T10:00:00Z');
189
+ expect(result?.expiresAt).toBe('2026-12-01T10:00:00Z');
190
+ });
191
+ });
192
+ // ── error paths ─────────────────────────────────────────────────────────────
193
+ describe('validateGoogleReceipt — error paths', () => {
194
+ it('returns null on Play API error response', async () => {
195
+ mockV2Fetch({}, { status: 410 });
196
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', makeGoogleConfig());
197
+ expect(result).toBeNull();
198
+ });
199
+ it('returns null when serviceAccountKey is missing', async () => {
200
+ const fetchSpy = vi.spyOn(global, 'fetch');
201
+ const result = await validateGoogleReceipt('tok', 'pro_monthly', { packageName: 'com.example.app' });
202
+ expect(result).toBeNull();
203
+ expect(fetchSpy).not.toHaveBeenCalled();
204
+ });
205
+ });
206
+ //# sourceMappingURL=google-subscriptions-v2.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-subscriptions-v2.test.js","sourceRoot":"","sources":["../../src/__tests__/google-subscriptions-v2.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Lifecycle state tests — grace_period / on_hold introduced as first-class
3
+ * SubscriptionStatus values.
4
+ *
5
+ * Covers:
6
+ * - Apple webhook: DID_FAIL_TO_RENEW (subtype GRACE_PERIOD vs none), GRACE_PERIOD_EXPIRED
7
+ * - Google webhook: SUBSCRIPTION_IN_GRACE_PERIOD, SUBSCRIPTION_ON_HOLD
8
+ * - status route: active=true for grace_period, active=false for on_hold
9
+ * - CONSUMPTION_REQUEST: invokes consumptionInfoProvider then PUTs to Apple
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=lifecycle-states.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-states.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/lifecycle-states.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}