backend-manager 5.0.122 → 5.0.124

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 (45) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/CLAUDE.md +18 -19
  3. package/README.md +7 -7
  4. package/package.json +1 -1
  5. package/src/manager/cron/daily/reset-usage.js +79 -73
  6. package/src/manager/cron/daily.js +2 -53
  7. package/src/manager/cron/frequent/abandoned-carts.js +148 -0
  8. package/src/manager/cron/frequent.js +3 -0
  9. package/src/manager/cron/runner.js +60 -0
  10. package/src/manager/events/firestore/payments-disputes/on-write.js +358 -0
  11. package/src/manager/events/firestore/payments-webhooks/analytics.js +245 -121
  12. package/src/manager/events/firestore/payments-webhooks/on-write.js +32 -2
  13. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +11 -34
  14. package/src/manager/helpers/analytics.js +2 -2
  15. package/src/manager/helpers/usage.js +44 -20
  16. package/src/manager/helpers/user.js +2 -1
  17. package/src/manager/index.js +10 -0
  18. package/src/manager/libraries/abandoned-cart-config.js +12 -0
  19. package/src/manager/libraries/email.js +5 -5
  20. package/src/manager/libraries/openai.js +76 -7
  21. package/src/manager/libraries/payment/discount-codes.js +40 -0
  22. package/src/manager/libraries/recaptcha.js +36 -0
  23. package/src/manager/routes/app/get.js +1 -1
  24. package/src/manager/routes/marketing/contact/post.js +11 -29
  25. package/src/manager/routes/payments/discount/get.js +22 -0
  26. package/src/manager/routes/payments/dispute-alert/post.js +93 -0
  27. package/src/manager/routes/payments/dispute-alert/processors/chargeblast.js +43 -0
  28. package/src/manager/routes/payments/intent/post.js +29 -0
  29. package/src/manager/routes/payments/intent/processors/chargebee.js +59 -7
  30. package/src/manager/routes/payments/intent/processors/stripe.js +55 -7
  31. package/src/manager/routes/test/usage/post.js +10 -6
  32. package/src/manager/schemas/payments/discount/get.js +9 -0
  33. package/src/manager/schemas/payments/dispute-alert/post.js +6 -0
  34. package/src/manager/schemas/payments/intent/post.js +16 -0
  35. package/src/test/runner.js +14 -4
  36. package/src/test/test-accounts.js +18 -0
  37. package/templates/backend-manager-config.json +7 -1
  38. package/templates/firestore.rules +9 -1
  39. package/test/_legacy/usage.js +5 -5
  40. package/test/routes/marketing/contact.js +3 -2
  41. package/test/routes/payments/discount.js +80 -0
  42. package/test/routes/payments/dispute-alert.js +271 -0
  43. package/test/routes/payments/intent.js +60 -0
  44. package/test/routes/test/usage.js +134 -30
  45. package/test/rules/payments-carts.js +371 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Test: POST /payments/dispute-alert
3
+ * Tests the dispute alert endpoint validates requests and saves to Firestore
4
+ */
5
+ module.exports = {
6
+ description: 'Dispute alert endpoint',
7
+ type: 'group',
8
+ timeout: 30000,
9
+
10
+ tests: [
11
+ {
12
+ name: 'rejects-missing-key',
13
+ auth: 'none',
14
+ async run({ http, assert }) {
15
+ const response = await http.as('none').post('payments/dispute-alert', {});
16
+
17
+ assert.isError(response, 401, 'Should reject missing key');
18
+ },
19
+ },
20
+
21
+ {
22
+ name: 'rejects-invalid-key',
23
+ auth: 'none',
24
+ async run({ http, assert }) {
25
+ const response = await http.as('none').post('payments/dispute-alert?key=wrong-key', {});
26
+
27
+ assert.isError(response, 401, 'Should reject invalid key');
28
+ },
29
+ },
30
+
31
+ {
32
+ name: 'rejects-unknown-provider',
33
+ auth: 'none',
34
+ async run({ http, assert }) {
35
+ const response = await http.as('none').post(`payments/dispute-alert?alerts=unknown&key=${process.env.BACKEND_MANAGER_KEY}`, {
36
+ id: '_test-dispute-unknown-provider',
37
+ card: '4242',
38
+ amount: 9.99,
39
+ transactionDate: '2026-01-15',
40
+ });
41
+
42
+ assert.isError(response, 400, 'Should reject unknown alert provider');
43
+ },
44
+ },
45
+
46
+ {
47
+ name: 'rejects-missing-id',
48
+ auth: 'none',
49
+ async run({ http, assert }) {
50
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
51
+ card: '4242',
52
+ amount: 9.99,
53
+ transactionDate: '2026-01-15',
54
+ });
55
+
56
+ assert.isError(response, 400, 'Should reject missing id');
57
+ },
58
+ },
59
+
60
+ {
61
+ name: 'rejects-missing-card',
62
+ auth: 'none',
63
+ async run({ http, assert }) {
64
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
65
+ id: '_test-dispute-no-card',
66
+ amount: 9.99,
67
+ transactionDate: '2026-01-15',
68
+ });
69
+
70
+ assert.isError(response, 400, 'Should reject missing card');
71
+ },
72
+ },
73
+
74
+ {
75
+ name: 'rejects-missing-amount',
76
+ auth: 'none',
77
+ async run({ http, assert }) {
78
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
79
+ id: '_test-dispute-no-amount',
80
+ card: '4242',
81
+ transactionDate: '2026-01-15',
82
+ });
83
+
84
+ assert.isError(response, 400, 'Should reject missing amount');
85
+ },
86
+ },
87
+
88
+ {
89
+ name: 'rejects-missing-transaction-date',
90
+ auth: 'none',
91
+ async run({ http, assert }) {
92
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
93
+ id: '_test-dispute-no-date',
94
+ card: '4242',
95
+ amount: 9.99,
96
+ });
97
+
98
+ assert.isError(response, 400, 'Should reject missing transactionDate');
99
+ },
100
+ },
101
+
102
+ {
103
+ name: 'accepts-valid-chargeblast-alert',
104
+ auth: 'none',
105
+ async run({ http, assert, firestore }) {
106
+ const alertId = '_test-dispute-valid';
107
+
108
+ // Clean up any existing doc
109
+ await firestore.delete(`payments-disputes/${alertId}`);
110
+
111
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
112
+ id: alertId,
113
+ card: '4242424242424242',
114
+ cardBrand: 'Visa',
115
+ amount: 29.99,
116
+ transactionDate: '2026-03-07 14:30:00',
117
+ processor: 'stripe',
118
+ });
119
+
120
+ assert.isSuccess(response, 'Should accept valid Chargeblast alert');
121
+ assert.equal(response.data.received, true, 'Should confirm receipt');
122
+
123
+ // Verify doc was saved to Firestore
124
+ const doc = await firestore.get(`payments-disputes/${alertId}`);
125
+ assert.ok(doc, 'Dispute doc should exist in Firestore');
126
+ assert.equal(doc.provider, 'chargeblast', 'Provider should be chargeblast');
127
+ assert.ok(
128
+ doc.status === 'pending' || doc.status === 'processing',
129
+ 'Status should be pending or processing',
130
+ );
131
+
132
+ // Verify normalized alert data
133
+ assert.equal(doc.alert.card.last4, '4242', 'Should extract last4 from full card number');
134
+ assert.equal(doc.alert.card.brand, 'visa', 'Should lowercase card brand');
135
+ assert.equal(doc.alert.amount, 29.99, 'Amount should be preserved');
136
+ assert.equal(doc.alert.transactionDate, '2026-03-07', 'Should extract date without time');
137
+ assert.equal(doc.alert.processor, 'stripe', 'Processor should be stripe');
138
+
139
+ // Verify raw payload is preserved
140
+ assert.ok(doc.raw, 'Raw payload should be preserved');
141
+ assert.equal(doc.raw.id, alertId, 'Raw id should match');
142
+
143
+ // Clean up
144
+ await firestore.delete(`payments-disputes/${alertId}`);
145
+ },
146
+ },
147
+
148
+ {
149
+ name: 'accepts-alert-with-last4-only',
150
+ auth: 'none',
151
+ async run({ http, assert, firestore }) {
152
+ const alertId = '_test-dispute-last4';
153
+
154
+ // Clean up any existing doc
155
+ await firestore.delete(`payments-disputes/${alertId}`);
156
+
157
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
158
+ id: alertId,
159
+ card: '1234',
160
+ amount: 9.99,
161
+ transactionDate: '2026-01-15',
162
+ });
163
+
164
+ assert.isSuccess(response, 'Should accept alert with card last4 only');
165
+
166
+ const doc = await firestore.get(`payments-disputes/${alertId}`);
167
+ assert.equal(doc.alert.card.last4, '1234', 'Should use card value as last4 when already 4 digits');
168
+ assert.equal(doc.alert.processor, 'stripe', 'Processor should default to stripe');
169
+
170
+ // Clean up
171
+ await firestore.delete(`payments-disputes/${alertId}`);
172
+ },
173
+ },
174
+
175
+ {
176
+ name: 'deduplicates-dispute-alerts',
177
+ auth: 'none',
178
+ async run({ http, assert, firestore }) {
179
+ const alertId = '_test-dispute-duplicate';
180
+
181
+ // Clean up any existing doc
182
+ await firestore.delete(`payments-disputes/${alertId}`);
183
+
184
+ // Send first alert
185
+ await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
186
+ id: alertId,
187
+ card: '4242',
188
+ amount: 29.99,
189
+ transactionDate: '2026-03-07',
190
+ });
191
+
192
+ // Send duplicate
193
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
194
+ id: alertId,
195
+ card: '4242',
196
+ amount: 29.99,
197
+ transactionDate: '2026-03-07',
198
+ });
199
+
200
+ assert.isSuccess(response, 'Duplicate should still return 200');
201
+ assert.equal(response.data.duplicate, true, 'Should indicate duplicate');
202
+
203
+ // Clean up
204
+ await firestore.delete(`payments-disputes/${alertId}`);
205
+ },
206
+ },
207
+
208
+ {
209
+ name: 'retries-failed-alerts',
210
+ auth: 'none',
211
+ async run({ http, assert, firestore }) {
212
+ const alertId = '_test-dispute-retry';
213
+
214
+ // Pre-seed a failed dispute
215
+ await firestore.set(`payments-disputes/${alertId}`, {
216
+ id: alertId,
217
+ status: 'failed',
218
+ error: 'Previous error',
219
+ });
220
+
221
+ // Send alert with same ID — should retry since previous status was 'failed'
222
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
223
+ id: alertId,
224
+ card: '4242',
225
+ amount: 29.99,
226
+ transactionDate: '2026-03-07',
227
+ });
228
+
229
+ assert.isSuccess(response, 'Should accept retry of failed alert');
230
+ assert.ok(!response.data.duplicate, 'Should not indicate duplicate for failed retry');
231
+
232
+ // Verify doc was updated
233
+ const doc = await firestore.get(`payments-disputes/${alertId}`);
234
+ assert.ok(
235
+ doc.status === 'pending' || doc.status === 'processing',
236
+ 'Status should be pending or processing after retry',
237
+ );
238
+
239
+ // Clean up
240
+ await firestore.delete(`payments-disputes/${alertId}`);
241
+ },
242
+ },
243
+
244
+ {
245
+ name: 'defaults-alerts-to-chargeblast',
246
+ auth: 'none',
247
+ async run({ http, assert, firestore }) {
248
+ const alertId = '_test-dispute-default-provider';
249
+
250
+ // Clean up any existing doc
251
+ await firestore.delete(`payments-disputes/${alertId}`);
252
+
253
+ // Send without alerts query param
254
+ const response = await http.as('none').post(`payments/dispute-alert?key=${process.env.BACKEND_MANAGER_KEY}`, {
255
+ id: alertId,
256
+ card: '4242',
257
+ amount: 9.99,
258
+ transactionDate: '2026-01-15',
259
+ });
260
+
261
+ assert.isSuccess(response, 'Should accept without explicit alerts param');
262
+
263
+ const doc = await firestore.get(`payments-disputes/${alertId}`);
264
+ assert.equal(doc.provider, 'chargeblast', 'Provider should default to chargeblast');
265
+
266
+ // Clean up
267
+ await firestore.delete(`payments-disputes/${alertId}`);
268
+ },
269
+ },
270
+ ],
271
+ };
@@ -113,6 +113,66 @@ module.exports = {
113
113
  },
114
114
  },
115
115
 
116
+ {
117
+ name: 'rejects-invalid-discount-code',
118
+ async run({ http, assert, config }) {
119
+ const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
120
+
121
+ const response = await http.as('basic').post('payments/intent', {
122
+ processor: 'stripe',
123
+ productId: paidProduct.id,
124
+ frequency: 'monthly',
125
+ discount: 'FAKECODE',
126
+ });
127
+
128
+ assert.isError(response, 400, 'Should reject invalid discount code');
129
+ },
130
+ },
131
+
132
+ {
133
+ name: 'saves-discount-to-intent-doc',
134
+ async run({ http, assert, config, firestore }) {
135
+ const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
136
+
137
+ const response = await http.as('journey-payments-intent-discount').post('payments/intent', {
138
+ processor: 'test',
139
+ productId: paidProduct.id,
140
+ frequency: 'monthly',
141
+ discount: 'FLASH20',
142
+ });
143
+
144
+ assert.isSuccess(response, 'Should succeed with valid discount');
145
+
146
+ // Verify discount was resolved and saved to intent doc
147
+ const intentDoc = await firestore.get(`payments-intents/${response.data.orderId}`);
148
+ assert.ok(intentDoc.discount, 'Discount should be saved on intent');
149
+ assert.equal(intentDoc.discount.code, 'FLASH20', 'Discount code should match');
150
+ assert.equal(intentDoc.discount.percent, 20, 'Discount percent should be 20');
151
+ assert.equal(intentDoc.discount.duration, 'once', 'Discount duration should be once');
152
+ },
153
+ },
154
+
155
+ {
156
+ name: 'saves-attribution-and-supplemental-to-intent-doc',
157
+ async run({ http, assert, config, firestore }) {
158
+ const paidProduct = config.payment.products.find(p => p.id !== 'basic' && p.prices);
159
+
160
+ const response = await http.as('journey-payments-intent-attribution').post('payments/intent', {
161
+ processor: 'test',
162
+ productId: paidProduct.id,
163
+ frequency: 'monthly',
164
+ attribution: { utm_source: 'test', utm_medium: 'unit-test' },
165
+ supplemental: { referral: 'friend' },
166
+ });
167
+
168
+ assert.isSuccess(response, 'Should succeed with attribution and supplemental');
169
+
170
+ const intentDoc = await firestore.get(`payments-intents/${response.data.orderId}`);
171
+ assert.equal(intentDoc.attribution.utm_source, 'test', 'Attribution utm_source should be saved');
172
+ assert.equal(intentDoc.supplemental.referral, 'friend', 'Supplemental should be saved');
173
+ },
174
+ },
175
+
116
176
  {
117
177
  name: 'succeeds-with-test-processor',
118
178
  async run({ http, assert, config, firestore, accounts, waitFor }) {
@@ -19,7 +19,8 @@ module.exports = {
19
19
  state.initialUsage = userDoc?.usage || {};
20
20
 
21
21
  // Store initial values for requests metric (may not exist yet)
22
- state.initialPeriod = state.initialUsage?.requests?.period || 0;
22
+ state.initialMonthly = state.initialUsage?.requests?.monthly || 0;
23
+ state.initialDaily = state.initialUsage?.requests?.daily || 0;
23
24
  state.initialTotal = state.initialUsage?.requests?.total || 0;
24
25
 
25
26
  assert.ok(true, 'Initial usage state captured');
@@ -42,12 +43,21 @@ module.exports = {
42
43
  assert.equal(response.data.metric, 'requests', 'Metric should be requests');
43
44
  assert.equal(response.data.amount, 1, 'Default amount should be 1');
44
45
 
45
- // Verify increment happened
46
+ // Verify monthly incremented
46
47
  assert.equal(
47
- response.data.after.period,
48
- response.data.before.period + 1,
49
- 'Period should be incremented by 1'
48
+ response.data.after.monthly,
49
+ response.data.before.monthly + 1,
50
+ 'Monthly should be incremented by 1'
50
51
  );
52
+
53
+ // Verify daily incremented
54
+ assert.equal(
55
+ response.data.after.daily,
56
+ response.data.before.daily + 1,
57
+ 'Daily should be incremented by 1'
58
+ );
59
+
60
+ // Verify total incremented
51
61
  assert.equal(
52
62
  response.data.after.total,
53
63
  response.data.before.total + 1,
@@ -69,15 +79,25 @@ module.exports = {
69
79
  assert.ok(userDoc?.usage?.requests, 'User should have requests usage');
70
80
 
71
81
  assert.equal(
72
- userDoc.usage.requests.period,
73
- state.afterFirstIncrement.period,
74
- 'Persisted period should match API response'
82
+ userDoc.usage.requests.monthly,
83
+ state.afterFirstIncrement.monthly,
84
+ 'Persisted monthly should match API response'
85
+ );
86
+ assert.equal(
87
+ userDoc.usage.requests.daily,
88
+ state.afterFirstIncrement.daily,
89
+ 'Persisted daily should match API response'
75
90
  );
76
91
  assert.equal(
77
92
  userDoc.usage.requests.total,
78
93
  state.afterFirstIncrement.total,
79
94
  'Persisted total should match API response'
80
95
  );
96
+
97
+ // Verify last timestamp exists
98
+ assert.ok(userDoc.usage.requests.last, 'Should have last object');
99
+ assert.ok(userDoc.usage.requests.last.timestamp, 'Should have last.timestamp');
100
+ assert.ok(userDoc.usage.requests.last.timestampUNIX, 'Should have last.timestampUNIX');
81
101
  },
82
102
  },
83
103
 
@@ -92,11 +112,16 @@ module.exports = {
92
112
  assert.isSuccess(response, 'Custom amount increment should succeed');
93
113
  assert.equal(response.data.amount, 5, 'Amount should be 5');
94
114
 
95
- // Verify increment happened with custom amount
115
+ // Verify all counters incremented by 5
116
+ assert.equal(
117
+ response.data.after.monthly,
118
+ response.data.before.monthly + 5,
119
+ 'Monthly should be incremented by 5'
120
+ );
96
121
  assert.equal(
97
- response.data.after.period,
98
- response.data.before.period + 5,
99
- 'Period should be incremented by 5'
122
+ response.data.after.daily,
123
+ response.data.before.daily + 5,
124
+ 'Daily should be incremented by 5'
100
125
  );
101
126
  assert.equal(
102
127
  response.data.after.total,
@@ -117,9 +142,14 @@ module.exports = {
117
142
  assert.ok(userDoc?.usage?.requests, 'User should have requests usage');
118
143
 
119
144
  assert.equal(
120
- userDoc.usage.requests.period,
121
- state.afterCustomAmount.period,
122
- 'Requests period should be persisted'
145
+ userDoc.usage.requests.monthly,
146
+ state.afterCustomAmount.monthly,
147
+ 'Requests monthly should be persisted'
148
+ );
149
+ assert.equal(
150
+ userDoc.usage.requests.daily,
151
+ state.afterCustomAmount.daily,
152
+ 'Requests daily should be persisted'
123
153
  );
124
154
  assert.equal(
125
155
  userDoc.usage.requests.total,
@@ -151,13 +181,19 @@ module.exports = {
151
181
  assert.isSuccess(response3, 'Third increment should succeed');
152
182
 
153
183
  // Verify accumulation: should be initial + 1 (test 2) + 5 (test 4) + 1 + 1 + 3 = initial + 11
154
- const expectedPeriod = state.initialPeriod + 11;
184
+ const expectedMonthly = state.initialMonthly + 11;
185
+ const expectedDaily = state.initialDaily + 11;
155
186
  const expectedTotal = state.initialTotal + 11;
156
187
 
157
188
  assert.equal(
158
- response3.data.after.period,
159
- expectedPeriod,
160
- `Period should accumulate to ${expectedPeriod}`
189
+ response3.data.after.monthly,
190
+ expectedMonthly,
191
+ `Monthly should accumulate to ${expectedMonthly}`
192
+ );
193
+ assert.equal(
194
+ response3.data.after.daily,
195
+ expectedDaily,
196
+ `Daily should accumulate to ${expectedDaily}`
161
197
  );
162
198
  assert.equal(
163
199
  response3.data.after.total,
@@ -180,9 +216,11 @@ module.exports = {
180
216
  assert.equal(response.data.authenticated, false, 'Should report as unauthenticated');
181
217
  assert.equal(response.data.key, state.unauthKey, 'Key should be unknown');
182
218
 
183
- // Verify increment happened (relative check — prior tests may have incremented too)
184
- state.unauthPeriod = response.data.after.period;
185
- assert.equal(response.data.after.period, response.data.before.period + 1, 'Period should increment by 1');
219
+ // Verify all counters incremented
220
+ assert.equal(response.data.after.monthly, response.data.before.monthly + 1, 'Monthly should increment by 1');
221
+ assert.equal(response.data.after.daily, response.data.before.daily + 1, 'Daily should increment by 1');
222
+
223
+ state.unauthMonthly = response.data.after.monthly;
186
224
  },
187
225
  },
188
226
 
@@ -194,22 +232,66 @@ module.exports = {
194
232
 
195
233
  assert.ok(usageDoc, 'Usage doc should exist in usage collection');
196
234
  assert.ok(usageDoc?.requests, 'Usage doc should have the requests metric');
197
- assert.equal(usageDoc.requests.period, state.unauthPeriod, 'Persisted period should match');
235
+ assert.equal(usageDoc.requests.monthly, state.unauthMonthly, 'Persisted monthly should match');
198
236
  },
199
237
  },
200
238
 
201
- // Test 9: Cleanup - trigger daily cron via PubSub to delete usage collection
239
+ // Test 9: Cron resets daily counters for authenticated users
202
240
  {
203
- name: 'cleanup-reset-usage',
204
- async run({ assert, firestore, state, waitFor, pubsub }) {
205
- // Verify unauthenticated usage doc exists before cron
206
- const beforeUsageDoc = await firestore.get(`usage/${state.unauthKey}`);
207
- assert.ok(beforeUsageDoc, 'Usage doc should exist before cron');
241
+ name: 'cron-resets-daily-counters',
242
+ async run({ assert, firestore, state, accounts, waitFor, pubsub }) {
243
+ // Verify daily counter is > 0 before cron
244
+ const beforeDoc = await firestore.get(`users/${accounts.basic.uid}`);
245
+ assert.ok(beforeDoc?.usage?.requests?.daily > 0, 'Daily counter should be > 0 before cron');
246
+
247
+ // Store monthly and total before cron (should NOT be reset by daily cron)
248
+ state.monthlyBeforeCron = beforeDoc.usage.requests.monthly;
249
+ state.totalBeforeCron = beforeDoc.usage.requests.total;
208
250
 
209
251
  // Trigger cron via PubSub
210
252
  await pubsub.trigger('bm_cronDaily');
211
253
 
212
- // Wait for cron to delete the usage collection doc (max 10 seconds)
254
+ // Wait for cron to reset daily counter
255
+ try {
256
+ await waitFor(
257
+ async () => {
258
+ const doc = await firestore.get(`users/${accounts.basic.uid}`);
259
+ return doc?.usage?.requests?.daily === 0;
260
+ },
261
+ 10000,
262
+ 500
263
+ );
264
+ assert.ok(true, 'Daily counter was reset to 0 by cron');
265
+ } catch (error) {
266
+ assert.fail('Daily counter should be reset to 0 within 10s');
267
+ }
268
+ },
269
+ },
270
+
271
+ // Test 10: Cron preserves monthly and total counters (non-1st of month)
272
+ {
273
+ name: 'cron-preserves-monthly-and-total',
274
+ async run({ assert, firestore, state, accounts }) {
275
+ const afterDoc = await firestore.get(`users/${accounts.basic.uid}`);
276
+
277
+ assert.equal(
278
+ afterDoc.usage.requests.monthly,
279
+ state.monthlyBeforeCron,
280
+ 'Monthly counter should be preserved after daily cron'
281
+ );
282
+ assert.equal(
283
+ afterDoc.usage.requests.total,
284
+ state.totalBeforeCron,
285
+ 'Total counter should be preserved after daily cron'
286
+ );
287
+ },
288
+ },
289
+
290
+ // Test 11: Cron deletes unauthenticated usage collection
291
+ {
292
+ name: 'cron-deletes-unauthenticated-usage',
293
+ async run({ assert, firestore, state, waitFor }) {
294
+ // The cron was already triggered in test 9, so the usage collection should be deleted
213
295
  try {
214
296
  await waitFor(
215
297
  async () => {
@@ -225,5 +307,27 @@ module.exports = {
225
307
  }
226
308
  },
227
309
  },
310
+
311
+ // Test 12: Daily counter accumulates after cron reset
312
+ {
313
+ name: 'daily-counter-accumulates-after-reset',
314
+ async run({ http, assert }) {
315
+ // After cron reset daily to 0, new increments should start from 0
316
+ const response = await http.as('basic').post('test/usage', {
317
+ amount: 3,
318
+ });
319
+
320
+ assert.isSuccess(response, 'Increment after cron reset should succeed');
321
+ assert.equal(response.data.before.daily, 0, 'Daily should be 0 after cron reset');
322
+ assert.equal(response.data.after.daily, 3, 'Daily should be 3 after increment');
323
+
324
+ // Monthly should have continued accumulating (not reset)
325
+ assert.equal(
326
+ response.data.after.monthly,
327
+ response.data.before.monthly + 3,
328
+ 'Monthly should continue accumulating'
329
+ );
330
+ },
331
+ },
228
332
  ],
229
333
  };