Skip to content

Commit 8303a27

Browse files
committed
only automatically collect payments from users if their balance goes below the minimum payment size; also, only cancel subscriptions if the amount owed in pending payments goes $1 below min payment size. unit tested.
1 parent 8287adf commit 8303a27

7 files changed

+114
-16
lines changed

src/packages/server/purchases/maintain-automatic-payments.test.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,7 @@ describe("testing automatic payments in several situations", () => {
199199
);
200200
collect.length = 0;
201201
await maintainAutomaticPayments();
202-
expect(collect).toEqual([
203-
{
204-
account_id,
205-
amount: pay_as_you_go_min_payment,
206-
},
207-
]);
202+
// DO NOT collect whne amount is less than pay as you go min
203+
expect(collect).toEqual([]);
208204
});
209205
});

src/packages/server/purchases/maintain-automatic-payments.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,16 @@ export default async function maintainAutomaticPayments() {
126126
logger.debug(
127127
"Since amount ",
128128
amount,
129-
" is positive, will try to collect automatically",
129+
" is positive, may try to collect automatically",
130130
);
131131

132132
if (amount < pay_as_you_go_min_payment) {
133133
logger.debug(
134-
"amount is below min payment, so we instead charge the min payment amount of ",
134+
"amount is below min payment, so we do not charge anything for now. the min payment amount is ",
135135
pay_as_you_go_min_payment,
136136
);
137-
amount = pay_as_you_go_min_payment;
137+
// amount = pay_as_you_go_min_payment;
138+
continue;
138139
}
139140

140141
// Now make the attempt. This might work quickly, it might take a day, it might

src/packages/server/purchases/maintain-subscriptions.test.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe("test renewSubscriptions", () => {
9696
expect(
9797
Math.abs(subs[0].current_period_start.valueOf() - Date.now()),
9898
).toBeLessThan(1000 * 3600 * 24 * 2);
99-
// the purchase should be pending so we don't have any money.
99+
// the purchase should be pending, since we don't have any money.
100100
const pool = getPool();
101101
const { rows } = await pool.query(
102102
"SELECT pending FROM purchases where id=$1",
@@ -316,3 +316,93 @@ describe("testing cancelAllPendingSubscriptions works as it should", () => {
316316
expect(subs[0].status).toBe("canceled");
317317
});
318318
});
319+
320+
describe("test renewSubscriptions doesn't cancel tiny subscription", () => {
321+
const account_id = uuid();
322+
const x: any = {};
323+
it("creates an account, license and subscription", async () => {
324+
await createAccount({
325+
email: "",
326+
password: "xyz",
327+
firstName: "Test",
328+
lastName: "User",
329+
account_id,
330+
});
331+
const info = getPurchaseInfo(license0);
332+
x.license_id = await createLicense(account_id, info);
333+
x.subscription_id = await createSubscription(
334+
{
335+
account_id,
336+
cost: 1, // way less than min payment
337+
interval: "month",
338+
current_period_start: dayjs().toDate(),
339+
current_period_end: dayjs().add(1, "month").toDate(),
340+
status: "active",
341+
metadata: { type: "license", license_id: x.license_id },
342+
latest_purchase_id: 0,
343+
},
344+
null,
345+
);
346+
});
347+
348+
it("modifies our subscription so the start date is a month ago and the end date is 12 hours from now", async () => {
349+
const pool = getPool();
350+
await pool.query(
351+
"UPDATE subscriptions SET current_period_start=$1, current_period_end=$2 WHERE id=$3",
352+
[
353+
dayjs().subtract(1, "month").toDate(),
354+
dayjs().add(12, "hour").toDate(),
355+
x.subscription_id,
356+
],
357+
);
358+
});
359+
360+
it("runs renewSubscriptions and checks that our subscription did renew", async () => {
361+
await test.renewSubscriptions();
362+
const subs = await getSubscriptions({ account_id });
363+
// within 3 days of a month from now:
364+
expect(
365+
Math.abs(
366+
subs[0].current_period_end.valueOf() -
367+
dayjs().add(1, "month").toDate().valueOf(),
368+
),
369+
).toBeLessThan(1000 * 3600 * 24 * 2);
370+
// within 3 days of now:
371+
expect(
372+
Math.abs(subs[0].current_period_start.valueOf() - Date.now()),
373+
).toBeLessThan(1000 * 3600 * 24 * 2);
374+
// the purchase should be pending, since we don't have any money.
375+
const pool = getPool();
376+
const { rows } = await pool.query(
377+
"SELECT pending FROM purchases where id=$1",
378+
[subs[0].latest_purchase_id],
379+
);
380+
expect(rows[0].pending).toBe(true);
381+
});
382+
383+
it("changes time of purchase back to slightly more than grace period and verifies that cancelAllPendingSubscriptions does NOT cancel the subscription", async () => {
384+
const grace = await test.getGracePeriodDays();
385+
const pool = getPool();
386+
await pool.query(
387+
`UPDATE purchases SET time=NOW() - interval '${
388+
grace + 1
389+
} days' WHERE id=$1`,
390+
[x.purchase_id],
391+
);
392+
await test.cancelAllPendingSubscriptions();
393+
const subs = await getSubscriptions({ account_id });
394+
expect(subs.length).toBe(1);
395+
expect(subs[0].status).toBe("active");
396+
});
397+
398+
it("changes amount of pending purchase to also be BIG, and then subscription should get canceled", async () => {
399+
const pool = getPool();
400+
await pool.query(`UPDATE purchases SET cost=1000 WHERE id=$1`, [
401+
x.purchase_id,
402+
]);
403+
await test.cancelAllPendingSubscriptions();
404+
const subs = await getSubscriptions({ account_id });
405+
expect(subs.length).toBe(1);
406+
expect(subs[0].status).toBe("active");
407+
});
408+
});

src/packages/server/purchases/maintain-subscriptions.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import renewSubscription from "@cocalc/server/purchases/renew-subscription";
55
import cancelSubscription from "./cancel-subscription";
66
import sendSubscriptionRenewalEmails from "./subscription-renewal-emails";
77
import { isEmailConfigured } from "@cocalc/server/email/send-email";
8+
import { getPendingBalance } from "./get-balance";
89

910
const logger = getLogger("purchases:maintain-subscriptions");
1011

@@ -147,9 +148,10 @@ async function cancelAllPendingSubscriptions() {
147148
const grace = await getGracePeriodDays();
148149

149150
const pool = getPool();
150-
const { rows } = await pool.query(`
151-
SELECT account_id, id as purchase_id FROM purchases WHERE pending=true AND time <= NOW() - interval '${grace} days' AND service = 'edit-license';
152-
`);
151+
const { rows } = await pool.query(
152+
`
153+
SELECT account_id, id as purchase_id FROM purchases WHERE pending=true AND time <= NOW() - interval '${grace} days' AND service = 'edit-license'`,
154+
);
153155
logger.debug(
154156
"cancelPendingSubscriptions -- pending subscription purchases = ",
155157
rows,
@@ -173,6 +175,15 @@ SELECT account_id, id as purchase_id FROM purchases WHERE pending=true AND time
173175
// [ ] TODO: send email when canceling a subscription/license this way
174176
// with instructions to restart it?
175177
async function cancelOnePendingSubscription({ account_id, purchase_id }) {
178+
// Do NOT both canceling any user subscription if their pending payments
179+
// total up to less than the pay as you go minimum (with a little slack).
180+
// This is because we don't automatically collect payments in such cases.
181+
// They might manually pay anyways, but we don't want to count on that.
182+
const pendingBalance = await getPendingBalance(account_id);
183+
const { pay_as_you_go_min_payment } = await getServerSettings();
184+
if (Math.abs(pendingBalance) <= pay_as_you_go_min_payment + 1) { // pandingBalance is actually <=0.
185+
return;
186+
}
176187
const client = await getTransactionClient();
177188
try {
178189
const x = await client.query(

src/packages/server/purchases/renew-subscription.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe("create a subscription, then renew it", () => {
3636
try {
3737
await renewSubscription({ account_id, subscription_id });
3838
} catch (e) {
39-
expect(e.message).toMatch("do not have enough credits");
39+
expect(e.message).toMatch("Please add");
4040
}
4141
//const sub = await getSubscription(subscription_id);
4242
});

src/packages/server/purchases/resume-subscription.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe("create a subscription, cancel it, then resume it", () => {
9999
try {
100100
await resumeSubscription({ account_id, subscription_id });
101101
} catch (e) {
102-
expect(e.message).toMatch("do not have enough credits");
102+
expect(e.message).toMatch("Please add at least $7.90 to your account.");
103103
}
104104
});
105105
});

src/packages/server/purchases/student-pay.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe("test studentPay behaves at it should in various scenarios", () => {
7676
try {
7777
await studentPay({ account_id, project_id });
7878
} catch (e) {
79-
expect(e.message).toMatch("do not have enough");
79+
expect(e.message).toMatch("Please add at least $21.20 to your account.");
8080
}
8181
});
8282

0 commit comments

Comments
 (0)