@saasicat/adapter-prisma 1.0.0-rc.4 → 1.0.0-rc.6

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/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- c870e387e5f23920d793f7e6cb321855019d14ba30e46ff17bdc06444867ce6a
1
+ 529e89589dea5797242ccc83833f15705b7fbf7c056430280539f88b62f3d037
package/dist/index.cjs CHANGED
@@ -4073,9 +4073,14 @@ var PrismaSubscriptionContractRepository = class {
4073
4073
  where: {
4074
4074
  id: contractId
4075
4075
  },
4076
+ // A null status leaves the column alone: the contract keeps whatever
4077
+ // it had — `active`, usually — and its new `effectiveUntil` is what
4078
+ // takes it out of the active lookup once that moment arrives.
4076
4079
  data: {
4077
4080
  effectiveUntil: data.effectiveUntil,
4078
- status: data.status
4081
+ ...data.status === null ? {} : {
4082
+ status: data.status
4083
+ }
4079
4084
  },
4080
4085
  include: {
4081
4086
  lineItems: true
@@ -4242,10 +4247,36 @@ var PrismaSubscriptionRepository = class {
4242
4247
  async countActiveByPlanKey(projectKey) {
4243
4248
  const db = this.db();
4244
4249
  const subscriptions = await this.subscriptions(db).findMany({
4250
+ // Status alone answers the wrong question. A cancellation that has
4251
+ // taken effect leaves the column at ACTIVE — nothing transitions it
4252
+ // — so a plan's tenant count in the SuperAdmin UI would carry every
4253
+ // customer who ever left.
4254
+ //
4255
+ // Three branches, because the effective date lives in two places.
4256
+ // On a row written before the fields separated it is `canceledAt`
4257
+ // and the second column is null; reading only the second excludes
4258
+ // that customer on the day they DECLARE, months before they leave,
4259
+ // and the migration guide promises no backfill is needed.
4245
4260
  where: {
4246
4261
  status: {
4247
4262
  in: ACTIVE_STATUSES
4248
- }
4263
+ },
4264
+ OR: [
4265
+ {
4266
+ canceledAt: null
4267
+ },
4268
+ {
4269
+ canceledEffectiveAt: {
4270
+ gt: /* @__PURE__ */ new Date()
4271
+ }
4272
+ },
4273
+ {
4274
+ canceledEffectiveAt: null,
4275
+ canceledAt: {
4276
+ gt: /* @__PURE__ */ new Date()
4277
+ }
4278
+ }
4279
+ ]
4249
4280
  },
4250
4281
  select: {
4251
4282
  planVersionId: true
@@ -4334,6 +4365,12 @@ var PrismaSubscriptionRepository = class {
4334
4365
  pendingPlan: row.pendingPlan,
4335
4366
  pendingEffectiveAt: row.pendingEffectiveAt,
4336
4367
  customLimits: row.customLimits ?? null,
4368
+ // Entitlement resolution ends a subscription by reading these. An
4369
+ // adapter that leaves them out grants a subscription that ended
4370
+ // last January everything it had — which is why the port requires
4371
+ // them rather than defaulting them to null here.
4372
+ canceledAt: row.canceledAt ?? null,
4373
+ canceledEffectiveAt: row.canceledEffectiveAt ?? null,
4337
4374
  planVersionId: row.planVersionId,
4338
4375
  planVersion: {
4339
4376
  planId: planKey,
@@ -4479,6 +4516,13 @@ var PrismaSubscriptionUsageAdapter = class {
4479
4516
  startedAt: subscription.startedAt,
4480
4517
  currentPeriodStart: subscription.currentPeriodStart ?? null,
4481
4518
  currentPeriodEnd: subscription.currentPeriodEnd ?? null,
4519
+ // Without these three the cancellation rules read a term that is
4520
+ // always null and the tenant page shows a cancelled subscription as
4521
+ // running: the fields exist on the record, the shipped adapter did
4522
+ // not map them, and every consumer using it got the defaults.
4523
+ minimumTermUntil: subscription.minimumTermUntil ?? null,
4524
+ canceledAt: subscription.canceledAt ?? null,
4525
+ canceledEffectiveAt: subscription.canceledEffectiveAt ?? null,
4482
4526
  pendingPlan: subscription.pendingPlan,
4483
4527
  pendingBillingCycle: subscription.pendingBillingCycle ?? null,
4484
4528
  pendingEffectiveAt: subscription.pendingEffectiveAt,
@@ -4670,35 +4714,46 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4670
4714
  } : {}
4671
4715
  };
4672
4716
  if (this.schema.tenantSubscription.synchronizePlanVersion) {
4673
- const current = await subscription.findUnique({
4717
+ const current2 = await subscription.findUnique({
4674
4718
  where: {
4675
4719
  tenantId
4676
4720
  }
4677
4721
  });
4678
- if (!current) {
4722
+ if (!current2) {
4679
4723
  throw new Error(`No subscription for tenant ${tenantId}.`);
4680
4724
  }
4681
4725
  const storagePlanId = await this.planBinding.toStoragePlanId(client, input.planId);
4682
4726
  data.planVersionId = await this.findTargetPlanVersionId(client, storagePlanId, input.periodStart ?? /* @__PURE__ */ new Date());
4683
- if (await this.pendingVersionBelongsToAnotherPlan(client, current.pendingPlanVersionId, storagePlanId)) {
4727
+ if (await this.pendingVersionBelongsToAnotherPlan(client, current2.pendingPlanVersionId, storagePlanId)) {
4684
4728
  Object.assign(data, clearedPendingVersionData());
4685
4729
  }
4686
4730
  }
4687
- const updated = await subscription.update({
4731
+ const claim = await subscription.updateMany({
4688
4732
  where: {
4689
- tenantId
4733
+ tenantId,
4734
+ canceledAt: input.expectedCanceledAt
4690
4735
  },
4691
4736
  data
4692
4737
  });
4738
+ const current = await subscription.findUnique({
4739
+ where: {
4740
+ tenantId
4741
+ }
4742
+ });
4743
+ if (!current) {
4744
+ throw new Error(`No subscription for tenant ${tenantId}.`);
4745
+ }
4693
4746
  return {
4694
- plan: updated.plan,
4695
- billingCycle: updated.billingCycle
4747
+ plan: current.plan,
4748
+ billingCycle: current.billingCycle,
4749
+ claimed: claim.count > 0
4696
4750
  };
4697
4751
  }
4698
4752
  async schedulePlanChange(tenantId, input) {
4699
- await this.subscription(this.prisma).update({
4753
+ const claim = await this.subscription(this.prisma).updateMany({
4700
4754
  where: {
4701
- tenantId
4755
+ tenantId,
4756
+ canceledAt: input.expectedCanceledAt
4702
4757
  },
4703
4758
  data: {
4704
4759
  pendingPlan: input.pendingPlan,
@@ -4706,6 +4761,9 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4706
4761
  pendingEffectiveAt: input.pendingEffectiveAt
4707
4762
  }
4708
4763
  });
4764
+ return {
4765
+ claimed: claim.count > 0
4766
+ };
4709
4767
  }
4710
4768
  async acceptPendingPlanVersion(tenantId, userId, now) {
4711
4769
  const subscription = this.subscription(this.prisma);
@@ -4772,12 +4830,30 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4772
4830
  const storagePlanId = await this.planBinding.toStoragePlanId(tx, input.planId);
4773
4831
  data.planVersionId = await this.findTargetPlanVersionId(tx, storagePlanId, input.periodStart ?? /* @__PURE__ */ new Date());
4774
4832
  }
4775
- const updated = await this.subscription(tx).update({
4833
+ const claim = await this.subscription(tx).updateMany({
4776
4834
  where: {
4777
- tenantId
4835
+ tenantId,
4836
+ canceledAt: input.expectedCanceledAt
4778
4837
  },
4779
4838
  data
4780
4839
  });
4840
+ const updated = await this.subscription(tx).findUnique({
4841
+ where: {
4842
+ tenantId
4843
+ }
4844
+ });
4845
+ if (!updated) {
4846
+ throw new Error(`No subscription for tenant ${tenantId}.`);
4847
+ }
4848
+ if (claim.count === 0) {
4849
+ return {
4850
+ plan: updated.plan,
4851
+ billingCycle: updated.billingCycle,
4852
+ subscriptionId: updated.id,
4853
+ promoRedemption: null,
4854
+ claimed: false
4855
+ };
4856
+ }
4781
4857
  let promoRedemption = null;
4782
4858
  if (redeemPromo) {
4783
4859
  promoRedemption = await redeemPromo(tx, updated.id);
@@ -4786,11 +4862,12 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4786
4862
  plan: updated.plan,
4787
4863
  billingCycle: updated.billingCycle,
4788
4864
  subscriptionId: updated.id,
4789
- promoRedemption
4865
+ promoRedemption,
4866
+ claimed: true
4790
4867
  };
4791
4868
  });
4792
4869
  }
4793
- async cancelSubscription(tenantId, immediate, now) {
4870
+ async cancelSubscription(tenantId, input) {
4794
4871
  const subscription = this.subscription(this.prisma);
4795
4872
  const sub = await subscription.findUnique({
4796
4873
  where: {
@@ -4800,19 +4877,38 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4800
4877
  if (!sub) {
4801
4878
  throw new Error(`No subscription for tenant ${tenantId}.`);
4802
4879
  }
4803
- const canceledAt = immediate ? now : sub.currentPeriodEnd ?? now;
4804
- const updated = await subscription.update({
4880
+ const claimed = await subscription.updateMany({
4805
4881
  where: {
4806
- tenantId
4882
+ tenantId,
4883
+ canceledAt: null,
4884
+ canceledEffectiveAt: null
4807
4885
  },
4808
4886
  data: {
4809
- canceledAt,
4810
- status: immediate ? "CANCELED" : sub.status
4887
+ canceledAt: input.canceledAt,
4888
+ canceledEffectiveAt: input.effectiveAt,
4889
+ // Absent means unchanged rather than null: an ordinary
4890
+ // cancellation does not touch the commitment it was measured
4891
+ // against, and writing the field on every call would erase a
4892
+ // term that is still running.
4893
+ ...input.minimumTermUntil ? {
4894
+ minimumTermUntil: input.minimumTermUntil
4895
+ } : {},
4896
+ status: input.terminateNow ? "CANCELED" : sub.status
4811
4897
  }
4812
4898
  });
4899
+ const current = await subscription.findUnique({
4900
+ where: {
4901
+ tenantId
4902
+ }
4903
+ });
4904
+ if (!current) {
4905
+ throw new Error(`No subscription for tenant ${tenantId}.`);
4906
+ }
4813
4907
  return {
4814
- canceledAt: updated.canceledAt,
4815
- status: updated.status
4908
+ canceledAt: current.canceledAt ?? null,
4909
+ canceledEffectiveAt: current.canceledEffectiveAt ?? null,
4910
+ status: current.status,
4911
+ alreadyCanceled: claimed.count === 0
4816
4912
  };
4817
4913
  }
4818
4914
  subscription(client) {
package/dist/index.d.cts CHANGED
@@ -24,6 +24,11 @@ interface SubscriptionRowLike {
24
24
  pilotEndsAt?: Date | null;
25
25
  currentPeriodStart?: Date | null;
26
26
  currentPeriodEnd?: Date | null;
27
+ /** End of what was committed to. Null on a trial or a pre-existing row. */
28
+ minimumTermUntil?: Date | null;
29
+ /** When a cancellation was declared, and when it lands. */
30
+ canceledAt?: Date | null;
31
+ canceledEffectiveAt?: Date | null;
27
32
  pendingBillingCycle?: string | null;
28
33
  pendingPlanVersionEffectiveAt?: Date | null;
29
34
  pendingPlanVersionAccepted?: boolean;
@@ -1190,9 +1195,12 @@ declare class PrismaTenantSubscriptionWriteAdapter implements TenantSubscription
1190
1195
  changePlanImmediate(tenantId: string, input: ImmediatePlanChangeInput): Promise<{
1191
1196
  plan: string;
1192
1197
  billingCycle: string;
1198
+ claimed: boolean;
1193
1199
  }>;
1194
1200
  private changePlanImmediateInClient;
1195
- schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<void>;
1201
+ schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<{
1202
+ claimed: boolean;
1203
+ }>;
1196
1204
  acceptPendingPlanVersion(tenantId: string, userId: string, now: Date): Promise<{
1197
1205
  accepted: boolean;
1198
1206
  acceptedAt: Date | null;
@@ -1200,9 +1208,16 @@ declare class PrismaTenantSubscriptionWriteAdapter implements TenantSubscription
1200
1208
  alreadyAccepted: boolean;
1201
1209
  }>;
1202
1210
  private applyOnboardingSelectionAtomic;
1203
- cancelSubscription(tenantId: string, immediate: boolean, now: Date): Promise<{
1211
+ cancelSubscription(tenantId: string, input: {
1212
+ canceledAt: Date;
1213
+ effectiveAt: Date;
1214
+ terminateNow: boolean;
1215
+ minimumTermUntil?: Date;
1216
+ }): Promise<{
1204
1217
  canceledAt: Date | null;
1218
+ canceledEffectiveAt: Date | null;
1205
1219
  status: string;
1220
+ alreadyCanceled: boolean;
1206
1221
  }>;
1207
1222
  private subscription;
1208
1223
  private planVersions;
package/dist/index.d.ts CHANGED
@@ -24,6 +24,11 @@ interface SubscriptionRowLike {
24
24
  pilotEndsAt?: Date | null;
25
25
  currentPeriodStart?: Date | null;
26
26
  currentPeriodEnd?: Date | null;
27
+ /** End of what was committed to. Null on a trial or a pre-existing row. */
28
+ minimumTermUntil?: Date | null;
29
+ /** When a cancellation was declared, and when it lands. */
30
+ canceledAt?: Date | null;
31
+ canceledEffectiveAt?: Date | null;
27
32
  pendingBillingCycle?: string | null;
28
33
  pendingPlanVersionEffectiveAt?: Date | null;
29
34
  pendingPlanVersionAccepted?: boolean;
@@ -1190,9 +1195,12 @@ declare class PrismaTenantSubscriptionWriteAdapter implements TenantSubscription
1190
1195
  changePlanImmediate(tenantId: string, input: ImmediatePlanChangeInput): Promise<{
1191
1196
  plan: string;
1192
1197
  billingCycle: string;
1198
+ claimed: boolean;
1193
1199
  }>;
1194
1200
  private changePlanImmediateInClient;
1195
- schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<void>;
1201
+ schedulePlanChange(tenantId: string, input: ScheduledPlanChangeInput): Promise<{
1202
+ claimed: boolean;
1203
+ }>;
1196
1204
  acceptPendingPlanVersion(tenantId: string, userId: string, now: Date): Promise<{
1197
1205
  accepted: boolean;
1198
1206
  acceptedAt: Date | null;
@@ -1200,9 +1208,16 @@ declare class PrismaTenantSubscriptionWriteAdapter implements TenantSubscription
1200
1208
  alreadyAccepted: boolean;
1201
1209
  }>;
1202
1210
  private applyOnboardingSelectionAtomic;
1203
- cancelSubscription(tenantId: string, immediate: boolean, now: Date): Promise<{
1211
+ cancelSubscription(tenantId: string, input: {
1212
+ canceledAt: Date;
1213
+ effectiveAt: Date;
1214
+ terminateNow: boolean;
1215
+ minimumTermUntil?: Date;
1216
+ }): Promise<{
1204
1217
  canceledAt: Date | null;
1218
+ canceledEffectiveAt: Date | null;
1205
1219
  status: string;
1220
+ alreadyCanceled: boolean;
1206
1221
  }>;
1207
1222
  private subscription;
1208
1223
  private planVersions;
package/dist/index.js CHANGED
@@ -4012,9 +4012,14 @@ var PrismaSubscriptionContractRepository = class {
4012
4012
  where: {
4013
4013
  id: contractId
4014
4014
  },
4015
+ // A null status leaves the column alone: the contract keeps whatever
4016
+ // it had — `active`, usually — and its new `effectiveUntil` is what
4017
+ // takes it out of the active lookup once that moment arrives.
4015
4018
  data: {
4016
4019
  effectiveUntil: data.effectiveUntil,
4017
- status: data.status
4020
+ ...data.status === null ? {} : {
4021
+ status: data.status
4022
+ }
4018
4023
  },
4019
4024
  include: {
4020
4025
  lineItems: true
@@ -4181,10 +4186,36 @@ var PrismaSubscriptionRepository = class {
4181
4186
  async countActiveByPlanKey(projectKey) {
4182
4187
  const db = this.db();
4183
4188
  const subscriptions = await this.subscriptions(db).findMany({
4189
+ // Status alone answers the wrong question. A cancellation that has
4190
+ // taken effect leaves the column at ACTIVE — nothing transitions it
4191
+ // — so a plan's tenant count in the SuperAdmin UI would carry every
4192
+ // customer who ever left.
4193
+ //
4194
+ // Three branches, because the effective date lives in two places.
4195
+ // On a row written before the fields separated it is `canceledAt`
4196
+ // and the second column is null; reading only the second excludes
4197
+ // that customer on the day they DECLARE, months before they leave,
4198
+ // and the migration guide promises no backfill is needed.
4184
4199
  where: {
4185
4200
  status: {
4186
4201
  in: ACTIVE_STATUSES
4187
- }
4202
+ },
4203
+ OR: [
4204
+ {
4205
+ canceledAt: null
4206
+ },
4207
+ {
4208
+ canceledEffectiveAt: {
4209
+ gt: /* @__PURE__ */ new Date()
4210
+ }
4211
+ },
4212
+ {
4213
+ canceledEffectiveAt: null,
4214
+ canceledAt: {
4215
+ gt: /* @__PURE__ */ new Date()
4216
+ }
4217
+ }
4218
+ ]
4188
4219
  },
4189
4220
  select: {
4190
4221
  planVersionId: true
@@ -4273,6 +4304,12 @@ var PrismaSubscriptionRepository = class {
4273
4304
  pendingPlan: row.pendingPlan,
4274
4305
  pendingEffectiveAt: row.pendingEffectiveAt,
4275
4306
  customLimits: row.customLimits ?? null,
4307
+ // Entitlement resolution ends a subscription by reading these. An
4308
+ // adapter that leaves them out grants a subscription that ended
4309
+ // last January everything it had — which is why the port requires
4310
+ // them rather than defaulting them to null here.
4311
+ canceledAt: row.canceledAt ?? null,
4312
+ canceledEffectiveAt: row.canceledEffectiveAt ?? null,
4276
4313
  planVersionId: row.planVersionId,
4277
4314
  planVersion: {
4278
4315
  planId: planKey,
@@ -4418,6 +4455,13 @@ var PrismaSubscriptionUsageAdapter = class {
4418
4455
  startedAt: subscription.startedAt,
4419
4456
  currentPeriodStart: subscription.currentPeriodStart ?? null,
4420
4457
  currentPeriodEnd: subscription.currentPeriodEnd ?? null,
4458
+ // Without these three the cancellation rules read a term that is
4459
+ // always null and the tenant page shows a cancelled subscription as
4460
+ // running: the fields exist on the record, the shipped adapter did
4461
+ // not map them, and every consumer using it got the defaults.
4462
+ minimumTermUntil: subscription.minimumTermUntil ?? null,
4463
+ canceledAt: subscription.canceledAt ?? null,
4464
+ canceledEffectiveAt: subscription.canceledEffectiveAt ?? null,
4421
4465
  pendingPlan: subscription.pendingPlan,
4422
4466
  pendingBillingCycle: subscription.pendingBillingCycle ?? null,
4423
4467
  pendingEffectiveAt: subscription.pendingEffectiveAt,
@@ -4609,35 +4653,46 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4609
4653
  } : {}
4610
4654
  };
4611
4655
  if (this.schema.tenantSubscription.synchronizePlanVersion) {
4612
- const current = await subscription.findUnique({
4656
+ const current2 = await subscription.findUnique({
4613
4657
  where: {
4614
4658
  tenantId
4615
4659
  }
4616
4660
  });
4617
- if (!current) {
4661
+ if (!current2) {
4618
4662
  throw new Error(`No subscription for tenant ${tenantId}.`);
4619
4663
  }
4620
4664
  const storagePlanId = await this.planBinding.toStoragePlanId(client, input.planId);
4621
4665
  data.planVersionId = await this.findTargetPlanVersionId(client, storagePlanId, input.periodStart ?? /* @__PURE__ */ new Date());
4622
- if (await this.pendingVersionBelongsToAnotherPlan(client, current.pendingPlanVersionId, storagePlanId)) {
4666
+ if (await this.pendingVersionBelongsToAnotherPlan(client, current2.pendingPlanVersionId, storagePlanId)) {
4623
4667
  Object.assign(data, clearedPendingVersionData());
4624
4668
  }
4625
4669
  }
4626
- const updated = await subscription.update({
4670
+ const claim = await subscription.updateMany({
4627
4671
  where: {
4628
- tenantId
4672
+ tenantId,
4673
+ canceledAt: input.expectedCanceledAt
4629
4674
  },
4630
4675
  data
4631
4676
  });
4677
+ const current = await subscription.findUnique({
4678
+ where: {
4679
+ tenantId
4680
+ }
4681
+ });
4682
+ if (!current) {
4683
+ throw new Error(`No subscription for tenant ${tenantId}.`);
4684
+ }
4632
4685
  return {
4633
- plan: updated.plan,
4634
- billingCycle: updated.billingCycle
4686
+ plan: current.plan,
4687
+ billingCycle: current.billingCycle,
4688
+ claimed: claim.count > 0
4635
4689
  };
4636
4690
  }
4637
4691
  async schedulePlanChange(tenantId, input) {
4638
- await this.subscription(this.prisma).update({
4692
+ const claim = await this.subscription(this.prisma).updateMany({
4639
4693
  where: {
4640
- tenantId
4694
+ tenantId,
4695
+ canceledAt: input.expectedCanceledAt
4641
4696
  },
4642
4697
  data: {
4643
4698
  pendingPlan: input.pendingPlan,
@@ -4645,6 +4700,9 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4645
4700
  pendingEffectiveAt: input.pendingEffectiveAt
4646
4701
  }
4647
4702
  });
4703
+ return {
4704
+ claimed: claim.count > 0
4705
+ };
4648
4706
  }
4649
4707
  async acceptPendingPlanVersion(tenantId, userId, now) {
4650
4708
  const subscription = this.subscription(this.prisma);
@@ -4711,12 +4769,30 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4711
4769
  const storagePlanId = await this.planBinding.toStoragePlanId(tx, input.planId);
4712
4770
  data.planVersionId = await this.findTargetPlanVersionId(tx, storagePlanId, input.periodStart ?? /* @__PURE__ */ new Date());
4713
4771
  }
4714
- const updated = await this.subscription(tx).update({
4772
+ const claim = await this.subscription(tx).updateMany({
4715
4773
  where: {
4716
- tenantId
4774
+ tenantId,
4775
+ canceledAt: input.expectedCanceledAt
4717
4776
  },
4718
4777
  data
4719
4778
  });
4779
+ const updated = await this.subscription(tx).findUnique({
4780
+ where: {
4781
+ tenantId
4782
+ }
4783
+ });
4784
+ if (!updated) {
4785
+ throw new Error(`No subscription for tenant ${tenantId}.`);
4786
+ }
4787
+ if (claim.count === 0) {
4788
+ return {
4789
+ plan: updated.plan,
4790
+ billingCycle: updated.billingCycle,
4791
+ subscriptionId: updated.id,
4792
+ promoRedemption: null,
4793
+ claimed: false
4794
+ };
4795
+ }
4720
4796
  let promoRedemption = null;
4721
4797
  if (redeemPromo) {
4722
4798
  promoRedemption = await redeemPromo(tx, updated.id);
@@ -4725,11 +4801,12 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4725
4801
  plan: updated.plan,
4726
4802
  billingCycle: updated.billingCycle,
4727
4803
  subscriptionId: updated.id,
4728
- promoRedemption
4804
+ promoRedemption,
4805
+ claimed: true
4729
4806
  };
4730
4807
  });
4731
4808
  }
4732
- async cancelSubscription(tenantId, immediate, now) {
4809
+ async cancelSubscription(tenantId, input) {
4733
4810
  const subscription = this.subscription(this.prisma);
4734
4811
  const sub = await subscription.findUnique({
4735
4812
  where: {
@@ -4739,19 +4816,38 @@ var PrismaTenantSubscriptionWriteAdapter = class {
4739
4816
  if (!sub) {
4740
4817
  throw new Error(`No subscription for tenant ${tenantId}.`);
4741
4818
  }
4742
- const canceledAt = immediate ? now : sub.currentPeriodEnd ?? now;
4743
- const updated = await subscription.update({
4819
+ const claimed = await subscription.updateMany({
4744
4820
  where: {
4745
- tenantId
4821
+ tenantId,
4822
+ canceledAt: null,
4823
+ canceledEffectiveAt: null
4746
4824
  },
4747
4825
  data: {
4748
- canceledAt,
4749
- status: immediate ? "CANCELED" : sub.status
4826
+ canceledAt: input.canceledAt,
4827
+ canceledEffectiveAt: input.effectiveAt,
4828
+ // Absent means unchanged rather than null: an ordinary
4829
+ // cancellation does not touch the commitment it was measured
4830
+ // against, and writing the field on every call would erase a
4831
+ // term that is still running.
4832
+ ...input.minimumTermUntil ? {
4833
+ minimumTermUntil: input.minimumTermUntil
4834
+ } : {},
4835
+ status: input.terminateNow ? "CANCELED" : sub.status
4750
4836
  }
4751
4837
  });
4838
+ const current = await subscription.findUnique({
4839
+ where: {
4840
+ tenantId
4841
+ }
4842
+ });
4843
+ if (!current) {
4844
+ throw new Error(`No subscription for tenant ${tenantId}.`);
4845
+ }
4752
4846
  return {
4753
- canceledAt: updated.canceledAt,
4754
- status: updated.status
4847
+ canceledAt: current.canceledAt ?? null,
4848
+ canceledEffectiveAt: current.canceledEffectiveAt ?? null,
4849
+ status: current.status,
4850
+ alreadyCanceled: claimed.count === 0
4755
4851
  };
4756
4852
  }
4757
4853
  subscription(client) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/adapter-prisma",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.6",
4
4
  "description": "Prisma + PostgreSQL persistence for SaaSiCat: one standard bundle for core, catalog, entitlement, tenant billing, Admin resources and promo, plus individual adapters for custom compositions.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -23,7 +23,7 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@saasicat/core": "^1.0.0-rc.4"
26
+ "@saasicat/core": "^1.0.0-rc.6"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@nestjs/common": "^11.0.0"
@@ -35,8 +35,8 @@
35
35
  "@prisma/client": "^6.19.0",
36
36
  "tsup": "^8.0.0",
37
37
  "typescript": "^6.0.0",
38
- "@saasicat/persistence-testing": "^1.0.0-rc.4",
39
- "@saasicat/spec": "^1.0.0-rc.4"
38
+ "@saasicat/persistence-testing": "^1.0.0-rc.6",
39
+ "@saasicat/spec": "^1.0.0-rc.6"
40
40
  },
41
41
  "license": "PolyForm-Shield-1.0.0",
42
42
  "author": "Taci Uelker",