@oneuptime/common 12.0.1 → 12.0.2
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.
- package/Models/DatabaseModels/Host.ts +12 -4
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +47 -5
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +47 -5
- package/Server/Services/DatabaseService.ts +87 -6
- package/Server/Services/DockerHostService.ts +47 -5
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +48 -5
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +48 -5
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +47 -5
- package/Server/Services/KubernetesClusterService.ts +48 -5
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +106 -1
- package/Server/Services/MonitorService.ts +57 -0
- package/Server/Services/PodmanHostService.ts +47 -5
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +47 -5
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +48 -5
- package/Server/Services/ServerlessFunctionService.ts +48 -5
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Date.ts +112 -0
- package/Types/Events/Recurring.ts +99 -1
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +42 -5
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +42 -5
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +69 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +42 -5
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +43 -5
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +42 -5
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +92 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +47 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +42 -5
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +43 -5
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +12 -1
|
@@ -22,6 +22,7 @@ import APIException from "../../Types/Exception/ApiException";
|
|
|
22
22
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
23
23
|
import ProductType from "../../Types/MeteredPlan/ProductType";
|
|
24
24
|
import ObjectID from "../../Types/ObjectID";
|
|
25
|
+
import Sleep from "../../Types/Sleep";
|
|
25
26
|
import Stripe from "stripe";
|
|
26
27
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
27
28
|
/*
|
|
@@ -30,6 +31,15 @@ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
|
30
31
|
* instead of a raw payment-provider failure.
|
|
31
32
|
*/
|
|
32
33
|
export const MAX_TRIAL_LENGTH_IN_DAYS = 730;
|
|
34
|
+
/*
|
|
35
|
+
* How long to wait before trying a cancel again, per retry.
|
|
36
|
+
*
|
|
37
|
+
* The errors that lose a cancel - a rate limit, a dropped connection, a
|
|
38
|
+
* transient 5xx - are the ones a second attempt fixes, and a cancel that never
|
|
39
|
+
* lands leaves an open subscription nothing in OneUptime points at any more.
|
|
40
|
+
* Kept short and finite because this runs inside the plan change request.
|
|
41
|
+
*/
|
|
42
|
+
const CANCEL_RETRY_DELAYS_IN_MS = [1000, 3000];
|
|
33
43
|
export class BillingService extends BaseService {
|
|
34
44
|
constructor() {
|
|
35
45
|
super();
|
|
@@ -203,6 +213,9 @@ export class BillingService extends BaseService {
|
|
|
203
213
|
meteredPlanSubscriptionParams.default_payment_method =
|
|
204
214
|
data.defaultPaymentMethodId;
|
|
205
215
|
}
|
|
216
|
+
if (data.metadata) {
|
|
217
|
+
meteredPlanSubscriptionParams.metadata = data.metadata;
|
|
218
|
+
}
|
|
206
219
|
// Create metered subscriptions
|
|
207
220
|
const meteredSubscription = await this.stripe.subscriptions.create(meteredPlanSubscriptionParams);
|
|
208
221
|
for (const serverMeteredPlan of data.serverMeteredPlans) {
|
|
@@ -245,6 +258,64 @@ export class BillingService extends BaseService {
|
|
|
245
258
|
else if (data.trial instanceof Date) {
|
|
246
259
|
trialDate = data.trial;
|
|
247
260
|
}
|
|
261
|
+
/*
|
|
262
|
+
* A trial is live when its end date is still ahead of us - nothing else
|
|
263
|
+
* decides it.
|
|
264
|
+
*
|
|
265
|
+
* The new plan's configured trial length is an input to how long a *fresh*
|
|
266
|
+
* trial runs (the boolean branch above). It must not decide whether an
|
|
267
|
+
* already-running trial handed over by changePlan survives: gating on it
|
|
268
|
+
* meant a project that changed plan mid-trial onto a plan configured with
|
|
269
|
+
* 0 trial days got trial_end "now", and Stripe invoiced the full plan on
|
|
270
|
+
* the spot. That is the upgrade-during-trial charge customers reported.
|
|
271
|
+
*
|
|
272
|
+
* Same predicate as subscribeToMeteredPlan, so the flat-fee and the
|
|
273
|
+
* metered subscription always end their trial on the same date.
|
|
274
|
+
*/
|
|
275
|
+
const isTrialing = Boolean(trialDate && OneUptimeDate.isInTheFuture(trialDate));
|
|
276
|
+
const subscriptionId = await this.createFlatFeeSubscription({
|
|
277
|
+
customerId: data.customerId,
|
|
278
|
+
plan: data.plan,
|
|
279
|
+
quantity: data.quantity,
|
|
280
|
+
isYearly: data.isYearly,
|
|
281
|
+
trialDate: isTrialing ? trialDate : null,
|
|
282
|
+
defaultPaymentMethodId: data.defaultPaymentMethodId,
|
|
283
|
+
promoCode: data.promoCode,
|
|
284
|
+
});
|
|
285
|
+
// Create metered subscriptions
|
|
286
|
+
const meteredSubscription = await this.subscribeToMeteredPlan(Object.assign(Object.assign({}, data), {
|
|
287
|
+
/*
|
|
288
|
+
* The resolved trial, not the raw one: both subscriptions have to answer
|
|
289
|
+
* "is this project trialing" the same way, or usage starts being invoiced
|
|
290
|
+
* while the flat fee is still waived.
|
|
291
|
+
*/
|
|
292
|
+
trialDate: isTrialing ? trialDate : null }));
|
|
293
|
+
return {
|
|
294
|
+
subscriptionId: subscriptionId,
|
|
295
|
+
meteredSubscriptionId: meteredSubscription.meteredSubscriptionId,
|
|
296
|
+
/*
|
|
297
|
+
* Reported with the same predicate that built trial_end. Callers persist
|
|
298
|
+
* this onto the project row, so a mismatch would leave the project
|
|
299
|
+
* showing no trial while Stripe has the subscription trialing (or the
|
|
300
|
+
* reverse) - and the project row is what gates the in-app trial banner.
|
|
301
|
+
*/
|
|
302
|
+
trialEndsAt: isTrialing ? trialDate : null,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/*
|
|
306
|
+
* Creates the flat-fee subscription that carries the plan's price.
|
|
307
|
+
*
|
|
308
|
+
* Split out of subscribeToPlan so a plan change can rebuild the flat-fee
|
|
309
|
+
* subscription on its own. The metered subscription is not part of a plan
|
|
310
|
+
* change - it carries the usage already reported for the current period -
|
|
311
|
+
* and is only ever rebuilt when it is itself dead.
|
|
312
|
+
*
|
|
313
|
+
* The trial is passed in already resolved. Whether one is running is the
|
|
314
|
+
* caller's decision, and it has to be the same decision the metered
|
|
315
|
+
* subscription is created with, or usage starts being invoiced while the
|
|
316
|
+
* flat fee is still waived.
|
|
317
|
+
*/
|
|
318
|
+
async createFlatFeeSubscription(data) {
|
|
248
319
|
const subscriptionParams = {
|
|
249
320
|
customer: data.customerId,
|
|
250
321
|
items: [
|
|
@@ -256,8 +327,9 @@ export class BillingService extends BaseService {
|
|
|
256
327
|
},
|
|
257
328
|
],
|
|
258
329
|
proration_behavior: "always_invoice",
|
|
259
|
-
|
|
260
|
-
|
|
330
|
+
// Same predicate as subscribeToMeteredPlan, so the pair trials together.
|
|
331
|
+
trial_end: data.trialDate && OneUptimeDate.isInTheFuture(data.trialDate)
|
|
332
|
+
? OneUptimeDate.toUnixTimestamp(data.trialDate)
|
|
261
333
|
: "now",
|
|
262
334
|
};
|
|
263
335
|
if (data.promoCode) {
|
|
@@ -266,14 +338,11 @@ export class BillingService extends BaseService {
|
|
|
266
338
|
if (data.defaultPaymentMethodId) {
|
|
267
339
|
subscriptionParams.default_payment_method = data.defaultPaymentMethodId;
|
|
268
340
|
}
|
|
341
|
+
if (data.metadata) {
|
|
342
|
+
subscriptionParams.metadata = data.metadata;
|
|
343
|
+
}
|
|
269
344
|
const subscription = await this.stripe.subscriptions.create(subscriptionParams);
|
|
270
|
-
|
|
271
|
-
const meteredSubscription = await this.subscribeToMeteredPlan(Object.assign(Object.assign({}, data), { trialDate }));
|
|
272
|
-
return {
|
|
273
|
-
subscriptionId: subscription.id,
|
|
274
|
-
meteredSubscriptionId: meteredSubscription.meteredSubscriptionId,
|
|
275
|
-
trialEndsAt: trialDate && data.plan.getTrialPeriod() > 0 ? trialDate : null,
|
|
276
|
-
};
|
|
345
|
+
return subscription.id;
|
|
277
346
|
}
|
|
278
347
|
async changeQuantity(subscriptionId, quantity) {
|
|
279
348
|
var _a;
|
|
@@ -396,8 +465,108 @@ export class BillingService extends BaseService {
|
|
|
396
465
|
}
|
|
397
466
|
return subscription.items.data;
|
|
398
467
|
}
|
|
468
|
+
/*
|
|
469
|
+
* The trial a subscription itself carries, as a Date. Null when it carries
|
|
470
|
+
* none - which is also what a subscription that no longer exists reports.
|
|
471
|
+
*/
|
|
472
|
+
getTrialEndOnSubscription(subscription) {
|
|
473
|
+
return (subscription === null || subscription === void 0 ? void 0 : subscription.trial_end)
|
|
474
|
+
? OneUptimeDate.fromUnixTimestamp(subscription.trial_end)
|
|
475
|
+
: null;
|
|
476
|
+
}
|
|
477
|
+
/*
|
|
478
|
+
* Whether a subscription is one the project has to keep.
|
|
479
|
+
*
|
|
480
|
+
* A subscription that still bills the customer - active, trialing, or
|
|
481
|
+
* past_due, the same three the rest of the product counts as active - is
|
|
482
|
+
* kept, and a plan change is applied to it in place. Cancelling one of
|
|
483
|
+
* these and creating a replacement bills the customer for both for as long
|
|
484
|
+
* as the cancel does not land, and Stripe is still collecting on a past_due
|
|
485
|
+
* subscription, so an abandoned one is not harmless either.
|
|
486
|
+
*
|
|
487
|
+
* The rest - cancelled, unpaid, incomplete, incomplete_expired, paused -
|
|
488
|
+
* bill nothing and cannot be revived by an update: swapping a price onto
|
|
489
|
+
* one would leave the project on a subscription that still bills nothing.
|
|
490
|
+
* Those are replaced.
|
|
491
|
+
*/
|
|
492
|
+
isSubscriptionStillBilling(subscription) {
|
|
493
|
+
if (!(subscription === null || subscription === void 0 ? void 0 : subscription.status)) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
/*
|
|
497
|
+
* isSubscriptionActive answers "yes" to an absent status - it is written
|
|
498
|
+
* for project rows, where no status means a project that predates the
|
|
499
|
+
* column. A subscription always has one, and the guard above has already
|
|
500
|
+
* dealt with a subscription that is not there at all.
|
|
501
|
+
*/
|
|
502
|
+
return SubscriptionStatusUtil.isSubscriptionActive(subscription.status);
|
|
503
|
+
}
|
|
504
|
+
/*
|
|
505
|
+
* Whether a subscription has already finished, and so has nothing left to
|
|
506
|
+
* cancel.
|
|
507
|
+
*
|
|
508
|
+
* Not the negation of isSubscriptionStillBilling: between the two sit the
|
|
509
|
+
* statuses that bill nothing but are not over either - unpaid, incomplete,
|
|
510
|
+
* paused - which are replaced AND still have to be cancelled, because the
|
|
511
|
+
* subscription is still open at the payment provider.
|
|
512
|
+
*
|
|
513
|
+
* A subscription that is already cancelled or expired is not sent to the
|
|
514
|
+
* provider to be cancelled again. The provider rejects that, and the reject
|
|
515
|
+
* is indistinguishable to the retry loop from a cancel that genuinely did
|
|
516
|
+
* not land: it would spend seconds retrying and then raise an alert asking
|
|
517
|
+
* an operator to cancel by hand something that is already cancelled. Null
|
|
518
|
+
* is the same case - findSubscription has already established the provider
|
|
519
|
+
* does not have it.
|
|
520
|
+
*/
|
|
521
|
+
isSubscriptionAlreadyFinished(subscription) {
|
|
522
|
+
return (!subscription ||
|
|
523
|
+
subscription.status === "canceled" ||
|
|
524
|
+
subscription.status === "incomplete_expired");
|
|
525
|
+
}
|
|
526
|
+
/*
|
|
527
|
+
* Retrieves a subscription, returning null only when the payment provider
|
|
528
|
+
* says it does not have it.
|
|
529
|
+
*
|
|
530
|
+
* A project row can point at an id the provider has since dropped, and that
|
|
531
|
+
* is a reason to give the project a new subscription rather than to fail its
|
|
532
|
+
* plan change outright.
|
|
533
|
+
*
|
|
534
|
+
* Every other error is thrown. A rate limit, a dropped connection or a
|
|
535
|
+
* transient 5xx says nothing about whether the subscription is live, and
|
|
536
|
+
* reading one as "it is gone" is how a perfectly healthy project got routed
|
|
537
|
+
* onto the path that cancels and recreates its subscriptions. Failing the
|
|
538
|
+
* plan change leaves the project exactly as it was, which is the safe answer
|
|
539
|
+
* when we cannot tell.
|
|
540
|
+
*/
|
|
541
|
+
async findSubscription(subscriptionId) {
|
|
542
|
+
try {
|
|
543
|
+
return (await this.stripe.subscriptions.retrieve(subscriptionId)) || null;
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
if (this.isResourceMissingError(err)) {
|
|
547
|
+
logger.debug(`Payment provider has no subscription ${subscriptionId}. It will be replaced.`);
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
logger.error(err, { subscriptionId });
|
|
551
|
+
throw err;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/*
|
|
555
|
+
* A payment provider error saying the object is not there.
|
|
556
|
+
*
|
|
557
|
+
* Matched on the error's shape rather than with instanceof: the error can be
|
|
558
|
+
* raised by a different copy of the stripe package than the one imported
|
|
559
|
+
* here, and instanceof would then quietly say no - turning a subscription
|
|
560
|
+
* that is simply gone into a failed plan change.
|
|
561
|
+
*/
|
|
562
|
+
isResourceMissingError(error) {
|
|
563
|
+
const providerError = (error || {});
|
|
564
|
+
return (providerError.code === "resource_missing" &&
|
|
565
|
+
(providerError.type === "StripeInvalidRequestError" ||
|
|
566
|
+
providerError.rawType === "invalid_request_error"));
|
|
567
|
+
}
|
|
399
568
|
async changePlan(data) {
|
|
400
|
-
var _a;
|
|
569
|
+
var _a, _b, _c;
|
|
401
570
|
logger.debug("Changing plan");
|
|
402
571
|
logger.debug(data);
|
|
403
572
|
if (!this.isBillingEnabled()) {
|
|
@@ -420,34 +589,285 @@ export class BillingService extends BaseService {
|
|
|
420
589
|
logger.debug("No payment methods");
|
|
421
590
|
throw new BadDataException(Errors.BillingService.NO_PAYMENTS_METHODS);
|
|
422
591
|
}
|
|
423
|
-
|
|
592
|
+
/*
|
|
593
|
+
* The trial the customer is really on lives on the subscription being
|
|
594
|
+
* changed; endTrialAt is only the caller's copy of it. Resolve both while
|
|
595
|
+
* the old subscription is still readable, and keep whichever runs longer.
|
|
596
|
+
*
|
|
597
|
+
* Changing plan must never shorten a trial. A caller holding a stale
|
|
598
|
+
* project row - or one that does not pass endTrialAt at all - would
|
|
599
|
+
* otherwise be able to end a trial the customer was promised, and Stripe
|
|
600
|
+
* would invoice the full plan on the spot.
|
|
601
|
+
*/
|
|
602
|
+
const trialEndOnCurrentSubscription = this.getTrialEndOnSubscription(subscription);
|
|
603
|
+
let endTrialAt = data.endTrialAt;
|
|
604
|
+
if (trialEndOnCurrentSubscription &&
|
|
605
|
+
(!endTrialAt ||
|
|
606
|
+
OneUptimeDate.isAfter(trialEndOnCurrentSubscription, endTrialAt))) {
|
|
607
|
+
endTrialAt = trialEndOnCurrentSubscription;
|
|
608
|
+
}
|
|
609
|
+
/*
|
|
610
|
+
* A trial that has already lapsed is not revived by a plan change - the
|
|
611
|
+
* customer is billed from now, which is what changing plan off a finished
|
|
612
|
+
* trial is meant to do.
|
|
613
|
+
*/
|
|
614
|
+
if (endTrialAt && !OneUptimeDate.isInTheFuture(endTrialAt)) {
|
|
615
|
+
endTrialAt = undefined;
|
|
616
|
+
}
|
|
617
|
+
/*
|
|
618
|
+
* The metered subscription is read but never rebuilt while it is alive.
|
|
619
|
+
* Its items come from the metered plans' own price ids, which have nothing
|
|
620
|
+
* to do with the plan being changed - recreating it would throw away the
|
|
621
|
+
* usage already reported against it for the current period. It is fetched
|
|
622
|
+
* here to find out whether it is one of the two halves that has to be
|
|
623
|
+
* replaced.
|
|
624
|
+
*/
|
|
625
|
+
const meteredSubscription = await this.findSubscription(data.meteredSubscriptionId);
|
|
626
|
+
const subscriptionItemId = (_a = subscription.items.data[0]) === null || _a === void 0 ? void 0 : _a.id;
|
|
627
|
+
/*
|
|
628
|
+
* A plan change is a price swap, so do exactly that whenever the project's
|
|
629
|
+
* subscription can take one. Cancelling and recreating it means every
|
|
630
|
+
* piece of subscription state - the trial, the billing cycle anchor,
|
|
631
|
+
* discounts, billing anchor history, reported usage - has to be rebuilt by
|
|
632
|
+
* hand, and the window between the create and the cancel is one the
|
|
633
|
+
* customer pays for twice.
|
|
634
|
+
*
|
|
635
|
+
* The two subscriptions are decided separately, because they fail
|
|
636
|
+
* separately: a project can be billing quite happily on its flat-fee
|
|
637
|
+
* subscription while its metered one is dead, and the reverse. Replacing
|
|
638
|
+
* the pair whenever either half was dead is what cancelled a live, billing
|
|
639
|
+
* subscription and left the customer paying for it alongside its
|
|
640
|
+
* replacement. Nothing that still bills is cancelled here.
|
|
641
|
+
*
|
|
642
|
+
* A subscription with no item has no handle to swap a price onto, so it is
|
|
643
|
+
* replaced whatever its status says. It charges nothing either way - the
|
|
644
|
+
* items are what carry the prices - so replacing it bills nobody twice.
|
|
645
|
+
*/
|
|
646
|
+
const canSwapPriceOnSubscription = Boolean(subscriptionItemId) &&
|
|
647
|
+
this.isSubscriptionStillBilling(subscription);
|
|
648
|
+
const canKeepMeteredSubscription = this.isSubscriptionStillBilling(meteredSubscription);
|
|
649
|
+
const customerId = subscription.customer.toString();
|
|
650
|
+
/*
|
|
651
|
+
* Order matters, and it is: update what is being kept, create what is
|
|
652
|
+
* replacing what is not, cancel what has been replaced.
|
|
653
|
+
*
|
|
654
|
+
* The updates come first because they are the only steps that can fail
|
|
655
|
+
* without leaving anything behind - the project is left on the
|
|
656
|
+
* subscriptions it already had. The creates come before the cancels so
|
|
657
|
+
* that a failure between them leaves the project on the subscriptions it
|
|
658
|
+
* had rather than on none at all.
|
|
659
|
+
*/
|
|
660
|
+
const subscriptionIdsToCancel = [];
|
|
661
|
+
let newSubscriptionId = data.subscriptionId;
|
|
662
|
+
let newMeteredSubscriptionId = data.meteredSubscriptionId;
|
|
663
|
+
if (canSwapPriceOnSubscription) {
|
|
664
|
+
await this.swapPlanPriceOnSubscription({
|
|
665
|
+
subscriptionId: data.subscriptionId,
|
|
666
|
+
subscriptionItemId: subscriptionItemId,
|
|
667
|
+
priceId: data.isYearly
|
|
668
|
+
? data.newPlan.getYearlyPlanId()
|
|
669
|
+
: data.newPlan.getMonthlyPlanId(),
|
|
670
|
+
quantity: data.quantity,
|
|
671
|
+
endTrialAt: endTrialAt,
|
|
672
|
+
trialEndOnSubscription: trialEndOnCurrentSubscription,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (canKeepMeteredSubscription) {
|
|
676
|
+
await this.moveTrialOnMeteredSubscription({
|
|
677
|
+
meteredSubscriptionId: data.meteredSubscriptionId,
|
|
678
|
+
meteredSubscription: meteredSubscription,
|
|
679
|
+
endTrialAt: endTrialAt,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
if (!canSwapPriceOnSubscription) {
|
|
683
|
+
logger.debug("Replacing the flat-fee subscription");
|
|
684
|
+
newSubscriptionId = await this.createFlatFeeSubscription({
|
|
685
|
+
customerId: customerId,
|
|
686
|
+
plan: data.newPlan,
|
|
687
|
+
quantity: data.quantity,
|
|
688
|
+
isYearly: data.isYearly,
|
|
689
|
+
trialDate: endTrialAt || null,
|
|
690
|
+
defaultPaymentMethodId: (_b = paymentMethods[0]) === null || _b === void 0 ? void 0 : _b.id,
|
|
691
|
+
metadata: this.getReplacementSubscriptionMetadata({
|
|
692
|
+
projectId: data.projectId,
|
|
693
|
+
replacedSubscriptionId: data.subscriptionId,
|
|
694
|
+
}),
|
|
695
|
+
});
|
|
696
|
+
if (!this.isSubscriptionAlreadyFinished(subscription)) {
|
|
697
|
+
subscriptionIdsToCancel.push(data.subscriptionId);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (!canKeepMeteredSubscription) {
|
|
701
|
+
logger.debug("Replacing the metered subscription");
|
|
702
|
+
const replacementMeteredSubscription = await this.subscribeToMeteredPlan({
|
|
703
|
+
projectId: data.projectId,
|
|
704
|
+
customerId: customerId,
|
|
705
|
+
serverMeteredPlans: data.serverMeteredPlans,
|
|
706
|
+
trialDate: endTrialAt || null,
|
|
707
|
+
defaultPaymentMethodId: (_c = paymentMethods[0]) === null || _c === void 0 ? void 0 : _c.id,
|
|
708
|
+
metadata: this.getReplacementSubscriptionMetadata({
|
|
709
|
+
projectId: data.projectId,
|
|
710
|
+
replacedSubscriptionId: data.meteredSubscriptionId,
|
|
711
|
+
}),
|
|
712
|
+
});
|
|
713
|
+
newMeteredSubscriptionId =
|
|
714
|
+
replacementMeteredSubscription.meteredSubscriptionId;
|
|
715
|
+
if (!this.isSubscriptionAlreadyFinished(meteredSubscription)) {
|
|
716
|
+
subscriptionIdsToCancel.push(data.meteredSubscriptionId);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const subscriptionIdsPendingCancellation = await this.cancelReplacedSubscriptions(subscriptionIdsToCancel);
|
|
720
|
+
return {
|
|
721
|
+
subscriptionId: newSubscriptionId,
|
|
722
|
+
meteredSubscriptionId: newMeteredSubscriptionId,
|
|
723
|
+
/*
|
|
724
|
+
* endTrialAt is the resolved trial: the longer of the caller's and the
|
|
725
|
+
* one the old subscription carried, and undefined once it has lapsed.
|
|
726
|
+
* It is what was sent to the payment provider on both halves, and the
|
|
727
|
+
* caller writes it onto the project row.
|
|
728
|
+
*/
|
|
729
|
+
trialEndsAt: endTrialAt,
|
|
730
|
+
subscriptionIdsPendingCancellation: subscriptionIdsPendingCancellation,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
/*
|
|
734
|
+
* The breadcrumb a replacement subscription carries at the payment provider.
|
|
735
|
+
*
|
|
736
|
+
* It is set when the replacement is created, which is before anything is
|
|
737
|
+
* cancelled and before the caller writes the new ids over the old ones. That
|
|
738
|
+
* ordering is the point: if the cancel is lost, or the plan change dies
|
|
739
|
+
* between the two creates, this is what is left to find the abandoned
|
|
740
|
+
* subscription - and the project it belongs to - with.
|
|
741
|
+
*/
|
|
742
|
+
getReplacementSubscriptionMetadata(data) {
|
|
743
|
+
return {
|
|
744
|
+
projectId: data.projectId.toString(),
|
|
745
|
+
replacedSubscriptionId: data.replacedSubscriptionId,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/*
|
|
749
|
+
* Puts the new plan's price on the subscription the project already has.
|
|
750
|
+
*
|
|
751
|
+
* The subscription survives, so everything it carries survives with it.
|
|
752
|
+
*/
|
|
753
|
+
async swapPlanPriceOnSubscription(data) {
|
|
754
|
+
logger.debug("Swapping plan price on subscription");
|
|
424
755
|
logger.debug(data.subscriptionId);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
subscriptionId: subscribeToPlan.subscriptionId,
|
|
447
|
-
meteredSubscriptionId: subscribeToPlan.meteredSubscriptionId,
|
|
448
|
-
trialEndsAt: subscribeToPlan.trialEndsAt || undefined,
|
|
756
|
+
const isTrialing = Boolean(data.endTrialAt);
|
|
757
|
+
const updateParams = {
|
|
758
|
+
items: [
|
|
759
|
+
{
|
|
760
|
+
/*
|
|
761
|
+
* The id of the item being replaced. It is not optional: without it
|
|
762
|
+
* Stripe ADDS the new price alongside the old one instead of
|
|
763
|
+
* swapping it, and the customer is billed for both plans.
|
|
764
|
+
*/
|
|
765
|
+
id: data.subscriptionItemId,
|
|
766
|
+
price: data.priceId,
|
|
767
|
+
quantity: data.quantity,
|
|
768
|
+
},
|
|
769
|
+
],
|
|
770
|
+
/*
|
|
771
|
+
* Prorations are recorded and settle on the next invoice. Never
|
|
772
|
+
* "always_invoice" - that raises an invoice on the spot, which is the
|
|
773
|
+
* surprise charge a plan change must not produce. A trialing
|
|
774
|
+
* subscription has nothing to prorate, so it asks for nothing.
|
|
775
|
+
*/
|
|
776
|
+
proration_behavior: isTrialing ? "none" : "create_prorations",
|
|
449
777
|
};
|
|
450
|
-
|
|
778
|
+
/*
|
|
779
|
+
* trial_end is deliberately absent unless the trial has to move forward.
|
|
780
|
+
* Omitting it is what leaves the subscription trialing on the trial it
|
|
781
|
+
* already has - sending a value re-anchors the billing cycle to it, and
|
|
782
|
+
* sending "now" ends the trial and invoices the plan immediately.
|
|
783
|
+
*
|
|
784
|
+
* It is sent only when the resolved trial outlasts the one the
|
|
785
|
+
* subscription carries: an extension recorded on the project but not yet
|
|
786
|
+
* pushed to the payment provider is still a trial the customer was
|
|
787
|
+
* promised, and the date returned below is written onto the project row,
|
|
788
|
+
* so the two have to agree.
|
|
789
|
+
*/
|
|
790
|
+
if (data.endTrialAt &&
|
|
791
|
+
(!data.trialEndOnSubscription ||
|
|
792
|
+
OneUptimeDate.isAfter(data.endTrialAt, data.trialEndOnSubscription))) {
|
|
793
|
+
updateParams.trial_end = OneUptimeDate.toUnixTimestamp(data.endTrialAt);
|
|
794
|
+
}
|
|
795
|
+
await this.stripe.subscriptions.update(data.subscriptionId, updateParams);
|
|
796
|
+
logger.debug("Swapped plan price on subscription");
|
|
797
|
+
}
|
|
798
|
+
/*
|
|
799
|
+
* Carries a trial that has moved forward onto the metered subscription.
|
|
800
|
+
*
|
|
801
|
+
* Both subscriptions bill the same customer for the same project, so they
|
|
802
|
+
* have to end their trial on the same date - see subscribeToPlan. The
|
|
803
|
+
* metered one is touched only for that; its plan is not part of a plan
|
|
804
|
+
* change, and its items are the metered plans' own price ids.
|
|
805
|
+
*/
|
|
806
|
+
async moveTrialOnMeteredSubscription(data) {
|
|
807
|
+
const trialEndOnMeteredSubscription = this.getTrialEndOnSubscription(data.meteredSubscription);
|
|
808
|
+
if (data.endTrialAt &&
|
|
809
|
+
(!trialEndOnMeteredSubscription ||
|
|
810
|
+
OneUptimeDate.isAfter(data.endTrialAt, trialEndOnMeteredSubscription))) {
|
|
811
|
+
await this.stripe.subscriptions.update(data.meteredSubscriptionId, {
|
|
812
|
+
trial_end: OneUptimeDate.toUnixTimestamp(data.endTrialAt),
|
|
813
|
+
proration_behavior: "none",
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
/*
|
|
818
|
+
* Cancels the subscriptions a plan change has replaced, and reports back the
|
|
819
|
+
* ones that are still there.
|
|
820
|
+
*
|
|
821
|
+
* Every id handed here belongs to a subscription that was not billing when
|
|
822
|
+
* the plan change read it - changePlan keeps anything that was - so a cancel
|
|
823
|
+
* that does not land is a loose end rather than a second charge. It still
|
|
824
|
+
* must not pass silently: the caller is about to write the replacement ids
|
|
825
|
+
* over these, and after that nothing in OneUptime points at them.
|
|
826
|
+
*
|
|
827
|
+
* The cancel is not allowed to throw, because by this point the replacements
|
|
828
|
+
* already exist. Failing here would mean the caller never records them, and
|
|
829
|
+
* the abandoned subscription would be the new, live pair instead of the dead
|
|
830
|
+
* one - strictly worse than the problem being solved.
|
|
831
|
+
*/
|
|
832
|
+
async cancelReplacedSubscriptions(subscriptionIds) {
|
|
833
|
+
const subscriptionIdsPendingCancellation = [];
|
|
834
|
+
for (const subscriptionId of subscriptionIds) {
|
|
835
|
+
if (!subscriptionId) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
logger.debug(`Cancelling replaced subscription ${subscriptionId}`);
|
|
839
|
+
const isCancelled = await this.cancelReplacedSubscription(subscriptionId);
|
|
840
|
+
if (!isCancelled) {
|
|
841
|
+
subscriptionIdsPendingCancellation.push(subscriptionId);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return subscriptionIdsPendingCancellation;
|
|
845
|
+
}
|
|
846
|
+
// Cancels one replaced subscription, retrying. True when it is gone.
|
|
847
|
+
async cancelReplacedSubscription(subscriptionId) {
|
|
848
|
+
for (let attempt = 0; attempt <= CANCEL_RETRY_DELAYS_IN_MS.length; attempt++) {
|
|
849
|
+
try {
|
|
850
|
+
await this.stripe.subscriptions.del(subscriptionId);
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
/*
|
|
855
|
+
* Already gone - cancelled by a webhook, by another request, or by an
|
|
856
|
+
* earlier attempt of this one whose response was lost. Gone is the
|
|
857
|
+
* state being asked for.
|
|
858
|
+
*/
|
|
859
|
+
if (this.isResourceMissingError(err)) {
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
logger.error(err, { subscriptionId });
|
|
863
|
+
const delayInMs = CANCEL_RETRY_DELAYS_IN_MS[attempt];
|
|
864
|
+
if (delayInMs === undefined) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
await Sleep.sleep(delayInMs);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return false;
|
|
451
871
|
}
|
|
452
872
|
async deletePaymentMethod(customerId, paymentMethodId) {
|
|
453
873
|
if (!this.isBillingEnabled()) {
|
|
@@ -574,6 +994,16 @@ export class BillingService extends BaseService {
|
|
|
574
994
|
// Balance is in cents, convert to dollars
|
|
575
995
|
return (customer.balance || 0) / 100;
|
|
576
996
|
}
|
|
997
|
+
/*
|
|
998
|
+
* Cancels a subscription, and carries on if it could not be cancelled.
|
|
999
|
+
*
|
|
1000
|
+
* The leniency is for deleting a project: the project row goes away either
|
|
1001
|
+
* way, and a failed cancel there is a billing loose end to chase, not a
|
|
1002
|
+
* reason to leave the project half-deleted. A caller that needs to know the
|
|
1003
|
+
* subscription really stopped billing must not use this - a plan change goes
|
|
1004
|
+
* through cancelReplacedSubscriptions, which retries and reports what is
|
|
1005
|
+
* left.
|
|
1006
|
+
*/
|
|
577
1007
|
async cancelSubscription(subscriptionId) {
|
|
578
1008
|
if (!this.isBillingEnabled()) {
|
|
579
1009
|
throw new BadDataException(Errors.BillingService.BILLING_NOT_ENABLED);
|