feat(billing): add dual formulas, disable 10yuan gift, retry auto-renew
Memind CI / Test, build, and release guards (push) Successful in 3m43s
Memind CI / Test, build, and release guards (push) Successful in 3m43s
Turn off the low-balance 10 CNY signup bonus while keeping the 5 CNY registration credit, split metering into independent formula A/B scopes, and retry expired auto-renew subscriptions after recharge or hourly worker. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -353,8 +353,11 @@ describe('createSubscriptionService', () => {
|
||||
|
||||
describe('processAutoRenewals', () => {
|
||||
it('uses DB-backed plan definitions for renewed subscriptions', async () => {
|
||||
const userId = 'user-lite-1';
|
||||
const expiredSub = makeSubRow({
|
||||
user_id: userId,
|
||||
plan_type: 'lite',
|
||||
status: 'expired',
|
||||
auto_renew: 1,
|
||||
expires_at: Date.now() - 1000,
|
||||
balance_cents: 9999,
|
||||
@@ -364,6 +367,9 @@ describe('createSubscriptionService', () => {
|
||||
async query(sql, params) {
|
||||
this.queries.push({ sql, params });
|
||||
if (sql.includes('SELECT balance_cents')) return [[{ balance_cents: 9999 }]];
|
||||
if (sql.includes('SELECT * FROM h5_subscriptions WHERE id = ?')) {
|
||||
return [[{ ...expiredSub, id: 'new-sub-id', status: 'active' }]];
|
||||
}
|
||||
return [{ affectedRows: 1 }];
|
||||
},
|
||||
async beginTransaction() {},
|
||||
@@ -372,8 +378,12 @@ describe('createSubscriptionService', () => {
|
||||
release() {},
|
||||
};
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
if (sql.includes('FROM h5_subscriptions s')) return [[expiredSub]];
|
||||
async query(sql, params = []) {
|
||||
if (sql.includes('SELECT DISTINCT s.user_id')) return [[{ user_id: userId }]];
|
||||
if (sql.includes('SELECT id FROM h5_subscriptions') && sql.includes('status = \'active\'')) return [[]];
|
||||
if (sql.includes('FROM h5_subscriptions s') && sql.includes('ORDER BY s.expires_at DESC')) {
|
||||
return [[expiredSub]];
|
||||
}
|
||||
return [[]];
|
||||
},
|
||||
async getConnection() { return conn; },
|
||||
@@ -407,5 +417,54 @@ describe('createSubscriptionService', () => {
|
||||
assert.equal(insert.params[5], 0);
|
||||
assert.equal(insert.params[9], 0.45);
|
||||
});
|
||||
|
||||
it('retries expired subscriptions when balance becomes sufficient', async () => {
|
||||
const userId = 'user-retry-1';
|
||||
let balance = 500;
|
||||
const expiredSub = makeSubRow({
|
||||
user_id: userId,
|
||||
plan_type: 'lite',
|
||||
status: 'expired',
|
||||
auto_renew: 1,
|
||||
expires_at: Date.now() - 1000,
|
||||
});
|
||||
const conn = {
|
||||
async query(sql) {
|
||||
if (sql.includes('SELECT balance_cents')) return [[{ balance_cents: balance }]];
|
||||
if (sql.includes('SELECT * FROM h5_subscriptions WHERE id = ?')) {
|
||||
return [[{ ...expiredSub, id: 'renewed-sub', status: 'active' }]];
|
||||
}
|
||||
return [{ affectedRows: 1 }];
|
||||
},
|
||||
async beginTransaction() {},
|
||||
async commit() {},
|
||||
async rollback() {},
|
||||
release() {},
|
||||
};
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
if (sql.includes('SELECT id FROM h5_subscriptions') && sql.includes('status = \'active\'')) return [[]];
|
||||
if (sql.includes('FROM h5_subscriptions s') && sql.includes('ORDER BY s.expires_at DESC')) {
|
||||
return [[expiredSub]];
|
||||
}
|
||||
return [[]];
|
||||
},
|
||||
async getConnection() { return conn; },
|
||||
};
|
||||
const svc = createSubscriptionService(pool, {
|
||||
getPlanAsync: async (planType) => planType === 'lite'
|
||||
? { priceCents: 990, periodTokens: 1000, periodImages: 0, overageRate: 1, periodDays: 30 }
|
||||
: null,
|
||||
});
|
||||
|
||||
const failed = await svc.processAutoRenewalsForUser(userId);
|
||||
assert.equal(failed.ok, false);
|
||||
assert.equal(failed.reason, 'insufficient_balance');
|
||||
|
||||
balance = 5000;
|
||||
const renewed = await svc.processAutoRenewalsForUser(userId);
|
||||
assert.equal(renewed.ok, true);
|
||||
assert.equal(renewed.balanceCents, 4010);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user