feat(server): early subscription for iap (#13826)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a mutation to request/apply a subscription by transaction ID
(client mutation and server operation), returning subscription details.
* **Bug Fixes / Improvements**
* More robust external subscription sync with safer conflict detection,
optional short-lived confirmation, improved parsing and error logging.
* **Chores**
  * Standardized time constants for clarity.
* **Tests**
* Updated subscription test data (expiration date) to reflect new
lifecycle expectations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-10-29 22:25:43 +08:00
committed by GitHub
parent e4b5b24fdd
commit 89cc9b072b
9 changed files with 256 additions and 51 deletions
@@ -22,6 +22,7 @@ import {
AccessDenied,
AuthenticationRequired,
FailedToCheckout,
InvalidSubscriptionParameters,
Throttle,
WorkspaceIdRequiredToUpdateTeamSubscription,
} from '../../base';
@@ -543,6 +544,56 @@ export class UserSubscriptionResolver {
});
}
@Throttle('strict')
@Mutation(() => [SubscriptionType], {
description: 'Request to apply the subscription in advance',
})
async requestApplySubscription(
@CurrentUser() user: CurrentUser,
@Args('transactionId') transactionId: string
): Promise<Subscription[]> {
if (!user) {
throw new AuthenticationRequired();
}
let existsSubscription = await this.db.subscription.findFirst({
where: { rcExternalRef: transactionId },
});
// subscription with the transactionId already exists
if (existsSubscription) {
if (existsSubscription.targetId !== user.id) {
throw new InvalidSubscriptionParameters();
} else {
this.normalizeSubscription(existsSubscription);
return [existsSubscription];
}
}
let current: Subscription[] = [];
try {
await this.rcHandler.syncAppUserWithExternalRef(user.id, transactionId);
current = await this.db.subscription.findMany({
where: {
targetId: user.id,
status: {
in: [
SubscriptionStatus.Active,
SubscriptionStatus.Trialing,
SubscriptionStatus.PastDue,
],
},
},
});
// ignore errors
} catch {}
current.forEach(subscription => this.normalizeSubscription(subscription));
return current;
}
@Throttle('strict')
@Mutation(() => [SubscriptionType], {
description: 'Refresh current user subscriptions and return latest.',