@opengeni/api-router 1.1.0 → 1.2.0
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/app.js +1 -1
- package/dist/{chunk-A4J3LBH6.js → chunk-BAZXDFT2.js} +185 -93
- package/dist/{chunk-A4J3LBH6.js.map → chunk-BAZXDFT2.js.map} +1 -1
- package/dist/credit-promotion-snapshot.d.ts +6 -0
- package/dist/index.js +1 -1
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/routes/billing.d.ts +3 -1
- package/package.json +20 -20
- package/src/credit-promotion-snapshot.ts +36 -0
- package/src/mcp/action-catalog.gen.ts +24 -16
- package/src/model-catalog.ts +1 -0
- package/src/routes/billing.ts +72 -15
- package/src/routes/managed-onboarding.ts +2 -0
- package/src/routes/organization-memberships.ts +2 -0
- package/src/routes/workspaces.ts +27 -12
package/src/routes/billing.ts
CHANGED
|
@@ -8,12 +8,15 @@ import {
|
|
|
8
8
|
OrganizationUsageWorkspacePageQuery,
|
|
9
9
|
type AccessContext,
|
|
10
10
|
type Permission,
|
|
11
|
+
type PromotionalCreditScope,
|
|
11
12
|
} from "@opengeni/contracts";
|
|
12
13
|
import { OrganizationModelUsageQuery } from "@opengeni/contracts/organization-model-usage";
|
|
13
|
-
import { configuredEntitlements } from "@opengeni/config";
|
|
14
|
+
import { configuredEntitlements, promotionalCreditScope } from "@opengeni/config";
|
|
15
|
+
import { creditScopeMetadata, creditScopeFromMetadata } from "../credit-promotion-snapshot";
|
|
14
16
|
import {
|
|
15
17
|
applyCreditLedgerEntry,
|
|
16
18
|
getBillingBalance,
|
|
19
|
+
readCreditPromotionPolicy,
|
|
17
20
|
getBillingCustomer,
|
|
18
21
|
getCreditLedgerEntry,
|
|
19
22
|
hasCreditLedgerEntry,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
getOrganizationUsageSummary,
|
|
24
27
|
getOrganizationUsageWorkspacePage,
|
|
25
28
|
withSessionRlsActorContext,
|
|
29
|
+
withRlsContext,
|
|
26
30
|
getManagedAccount,
|
|
27
31
|
markStripeWebhookProcessed,
|
|
28
32
|
recordStripeWebhookEvent,
|
|
@@ -134,6 +138,15 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
134
138
|
const promotion = body.promotionCode
|
|
135
139
|
? await resolveCheckoutPromotionCode(stripe, body.promotionCode)
|
|
136
140
|
: null;
|
|
141
|
+
const promotionalScope = promotion
|
|
142
|
+
? promotionalCreditScope(
|
|
143
|
+
{
|
|
144
|
+
creditPromotionPolicy:
|
|
145
|
+
(await readCreditPromotionPolicy(deps.db)) ?? deps.settings.creditPromotionPolicy,
|
|
146
|
+
},
|
|
147
|
+
promotion.couponId,
|
|
148
|
+
)
|
|
149
|
+
: undefined;
|
|
137
150
|
const amountCents =
|
|
138
151
|
body.amountUsd !== undefined
|
|
139
152
|
? usdToCents(body.amountUsd)
|
|
@@ -150,6 +163,15 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
150
163
|
message: "Credits must be between $5 and $10,000.",
|
|
151
164
|
});
|
|
152
165
|
}
|
|
166
|
+
if (
|
|
167
|
+
promotionalScope &&
|
|
168
|
+
(!promotion?.amountOffCents || amountCents !== promotion.amountOffCents)
|
|
169
|
+
) {
|
|
170
|
+
throw new HTTPException(422, {
|
|
171
|
+
message:
|
|
172
|
+
"Redeem this offer for its exact credit amount. Buy additional credits separately.",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
153
175
|
const amountMicros = centsToMicros(amountCents);
|
|
154
176
|
const customerId = await getOrCreateStripeCustomer(deps, stripe, context, accountId);
|
|
155
177
|
const idempotencyKey = `checkout:${accountId}:${amountMicros}:${crypto.randomUUID()}`;
|
|
@@ -167,9 +189,11 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
167
189
|
successUrl: body.successUrl,
|
|
168
190
|
cancelUrl: body.cancelUrl,
|
|
169
191
|
idempotencyKey,
|
|
192
|
+
promotionalScope,
|
|
170
193
|
...(promotion
|
|
171
194
|
? {
|
|
172
195
|
promotionCodeId: promotion.id,
|
|
196
|
+
couponId: promotion.couponId,
|
|
173
197
|
fullyDiscounted:
|
|
174
198
|
promotion.percentOff === 100 || (promotion.amountOffCents ?? 0) >= amountCents,
|
|
175
199
|
}
|
|
@@ -193,6 +217,7 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
193
217
|
checkoutSessionId: session.id,
|
|
194
218
|
url: session.url,
|
|
195
219
|
amountUsd: amountCents / 100,
|
|
220
|
+
promotionalScope,
|
|
196
221
|
}),
|
|
197
222
|
);
|
|
198
223
|
});
|
|
@@ -241,6 +266,7 @@ export function registerBillingRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
241
266
|
state: entry ? "granted" : "pending",
|
|
242
267
|
amountMicros: entry?.amountMicros ?? credit.amountMicros,
|
|
243
268
|
currency: "usd",
|
|
269
|
+
promotionalScope: creditScopeFromMetadata(session.metadata),
|
|
244
270
|
free: entry
|
|
245
271
|
? entry.sourceType === "stripe_checkout_coupon"
|
|
246
272
|
: isFreeCouponCheckout(session),
|
|
@@ -340,6 +366,8 @@ export function stripeCheckoutSessionCreateParams(input: {
|
|
|
340
366
|
idempotencyKey: string;
|
|
341
367
|
/** Apply this promotion code up front instead of letting Stripe ask for one. */
|
|
342
368
|
promotionCodeId?: string | undefined;
|
|
369
|
+
couponId?: string | undefined;
|
|
370
|
+
promotionalScope?: PromotionalCreditScope | undefined;
|
|
343
371
|
/**
|
|
344
372
|
* The applied code covers the whole package, so the total is $0. Nothing is
|
|
345
373
|
* taxable, so Checkout skips tax and with it the billing address: the
|
|
@@ -366,7 +394,7 @@ export function stripeCheckoutSessionCreateParams(input: {
|
|
|
366
394
|
// Stripe takes either a code field on its page or one discount up front.
|
|
367
395
|
...(input.promotionCodeId
|
|
368
396
|
? { discounts: [{ promotion_code: input.promotionCodeId }] }
|
|
369
|
-
: { allow_promotion_codes:
|
|
397
|
+
: { allow_promotion_codes: false }),
|
|
370
398
|
customer: input.customerId,
|
|
371
399
|
customer_update: {
|
|
372
400
|
address: "auto",
|
|
@@ -397,6 +425,8 @@ export function stripeCheckoutSessionCreateParams(input: {
|
|
|
397
425
|
},
|
|
398
426
|
],
|
|
399
427
|
metadata: {
|
|
428
|
+
...creditScopeMetadata(input.promotionalScope),
|
|
429
|
+
...(input.couponId ? { opengeni_credit_offer_id: input.couponId } : {}),
|
|
400
430
|
opengeni_account_id: input.accountId,
|
|
401
431
|
opengeni_credit_amount_usd: (input.amountCents / 100).toFixed(2),
|
|
402
432
|
opengeni_credit_micros: String(input.amountMicros),
|
|
@@ -607,6 +637,10 @@ async function grantCheckoutSessionCredits(
|
|
|
607
637
|
}
|
|
608
638
|
const credit = decision.credit;
|
|
609
639
|
const freeCouponCheckout = isFreeCouponCheckout(session);
|
|
640
|
+
const promotionalScope = creditScopeFromMetadata(session.metadata);
|
|
641
|
+
if (promotionalScope && !freeCouponCheckout) {
|
|
642
|
+
throw new Error("Scoped promotional credits require a fully discounted checkout");
|
|
643
|
+
}
|
|
610
644
|
if (!(await getManagedAccount(deps.db, credit.accountId))) {
|
|
611
645
|
// Another OpenGeni deployment sharing the Stripe account (or an account
|
|
612
646
|
// removed before a delayed payment settled). Retrying cannot succeed, so
|
|
@@ -636,10 +670,13 @@ async function grantCheckoutSessionCredits(
|
|
|
636
670
|
accountId: credit.accountId,
|
|
637
671
|
type: freeCouponCheckout ? "grant" : "credit_topup",
|
|
638
672
|
amountMicros: credit.amountMicros,
|
|
673
|
+
eligibleModelIds: promotionalScope?.eligibleModelIds,
|
|
639
674
|
sourceType: freeCouponCheckout ? "stripe_checkout_coupon" : "stripe_checkout_session",
|
|
640
675
|
sourceId: session.id,
|
|
641
676
|
idempotencyKey: credit.idempotencyKey,
|
|
642
677
|
metadata: {
|
|
678
|
+
...(promotionalScope ? { creditOfferLabel: promotionalScope.label } : {}),
|
|
679
|
+
creditOfferId: session.metadata?.opengeni_credit_offer_id ?? null,
|
|
643
680
|
stripeEventId: source.stripeEventId,
|
|
644
681
|
stripePaymentIntentId:
|
|
645
682
|
typeof session.payment_intent === "string"
|
|
@@ -665,7 +702,12 @@ async function grantCheckoutSessionCredits(
|
|
|
665
702
|
async function resolveCheckoutPromotionCode(
|
|
666
703
|
stripe: Stripe,
|
|
667
704
|
code: string,
|
|
668
|
-
): Promise<{
|
|
705
|
+
): Promise<{
|
|
706
|
+
id: string;
|
|
707
|
+
couponId: string;
|
|
708
|
+
amountOffCents: number | null;
|
|
709
|
+
percentOff: number | null;
|
|
710
|
+
}> {
|
|
669
711
|
const listed = await stripe.promotionCodes.list({
|
|
670
712
|
code,
|
|
671
713
|
active: true,
|
|
@@ -684,7 +726,12 @@ async function resolveCheckoutPromotionCode(
|
|
|
684
726
|
coupon.amount_off && coupon.currency === "usd"
|
|
685
727
|
? coupon.amount_off
|
|
686
728
|
: (coupon.currency_options?.usd?.amount_off ?? null);
|
|
687
|
-
return {
|
|
729
|
+
return {
|
|
730
|
+
id: promotionCode.id,
|
|
731
|
+
couponId: coupon.id,
|
|
732
|
+
amountOffCents,
|
|
733
|
+
percentOff: coupon.percent_off ?? null,
|
|
734
|
+
};
|
|
688
735
|
}
|
|
689
736
|
|
|
690
737
|
async function mirrorPaymentIntentCustomer(deps: ApiRouteDeps, event: Stripe.Event): Promise<void> {
|
|
@@ -792,18 +839,28 @@ async function releaseDisputedCredits(
|
|
|
792
839
|
return;
|
|
793
840
|
}
|
|
794
841
|
const holdIdempotencyKey = `stripe:dispute_hold:${dispute.id}`;
|
|
795
|
-
if (!(await hasCreditLedgerEntry(deps.db, accountId, holdIdempotencyKey))) {
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
842
|
const debitMicros = await creditAdjustmentMicros(deps, metadata, dispute.amount);
|
|
799
|
-
await
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
843
|
+
await withRlsContext(deps.db, { accountId }, async (tx) => {
|
|
844
|
+
// Stripe can deliver the resolution before the hold. Materialize both
|
|
845
|
+
// receipts atomically so a late hold cannot withhold restored credits.
|
|
846
|
+
await applyCreditLedgerEntry(tx, {
|
|
847
|
+
accountId,
|
|
848
|
+
type: "credit_dispute_hold",
|
|
849
|
+
amountMicros: -debitMicros,
|
|
850
|
+
sourceType: "stripe_dispute",
|
|
851
|
+
sourceId: dispute.id,
|
|
852
|
+
idempotencyKey: holdIdempotencyKey,
|
|
853
|
+
metadata: { stripeDisputeId: dispute.id, stripeEventType: event.type },
|
|
854
|
+
});
|
|
855
|
+
await applyCreditLedgerEntry(tx, {
|
|
856
|
+
accountId,
|
|
857
|
+
type: "credit_dispute_release",
|
|
858
|
+
amountMicros: debitMicros,
|
|
859
|
+
sourceType: "stripe_dispute",
|
|
860
|
+
sourceId: dispute.id,
|
|
861
|
+
idempotencyKey: `stripe:dispute_release:${dispute.id}`,
|
|
862
|
+
metadata: { stripeDisputeId: dispute.id, stripeEventType: event.type },
|
|
863
|
+
});
|
|
807
864
|
});
|
|
808
865
|
}
|
|
809
866
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { signupCreditModelIds } from "@opengeni/config";
|
|
1
2
|
import {
|
|
2
3
|
CompleteSelfServiceOrganizationSetupRequest,
|
|
3
4
|
CompleteSelfServiceOrganizationSetupResponse,
|
|
@@ -80,6 +81,7 @@ export function registerManagedOnboardingRoutes(
|
|
|
80
81
|
organizationName,
|
|
81
82
|
operationId: parsed.data.operationId,
|
|
82
83
|
requestFingerprint,
|
|
84
|
+
trialCreditModelIds: signupCreditModelIds(deps.settings),
|
|
83
85
|
trialCreditsEnabled:
|
|
84
86
|
deps.settings.productAccessMode === "managed" &&
|
|
85
87
|
deps.settings.verifiedSignupTrialCreditsEnabled,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { signupCreditModelIds } from "@opengeni/config";
|
|
1
2
|
import {
|
|
2
3
|
lookupExternalIdentityForRequest,
|
|
3
4
|
cancelExternalWorkspaceMemberGrantForRequest,
|
|
@@ -310,6 +311,7 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
|
|
|
310
311
|
subjectId,
|
|
311
312
|
subjectLabel: session.user.email || session.user.name,
|
|
312
313
|
...payload,
|
|
314
|
+
trialCreditModelIds: signupCreditModelIds(deps.settings),
|
|
313
315
|
trialCreditsEnabled:
|
|
314
316
|
deps.settings.productAccessMode === "managed" &&
|
|
315
317
|
deps.settings.verifiedSignupTrialCreditsEnabled,
|
package/src/routes/workspaces.ts
CHANGED
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
import { loadWorkspaceCodexModelAvailability } from "@opengeni/core";
|
|
49
49
|
import {
|
|
50
50
|
allWorkspacePermissions,
|
|
51
|
+
getBillingBalance,
|
|
52
|
+
spendableCreditMicros,
|
|
51
53
|
createWorkspace,
|
|
52
54
|
ensureWorkspaceByExternalIdentity,
|
|
53
55
|
findWorkspaceByExternalIdentity,
|
|
@@ -621,25 +623,38 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
621
623
|
// The same precedence the server applies when a new chat, API create, or
|
|
622
624
|
// scheduled occurrence names no model; published so pickers show it.
|
|
623
625
|
const workspaceSettings = workspace?.settings ?? {};
|
|
626
|
+
const creditBalance =
|
|
627
|
+
deps.settings.billingMode === "stripe"
|
|
628
|
+
? await getBillingBalance(deps.db, grant.accountId)
|
|
629
|
+
: undefined;
|
|
624
630
|
const defaultSelection = await resolveDefaultSessionModelForSelections(deps.db, {
|
|
625
631
|
settings: workspaceCatalogSettings,
|
|
626
632
|
accountId: grant.accountId,
|
|
627
633
|
workspaceSettings,
|
|
628
634
|
selections,
|
|
629
635
|
});
|
|
636
|
+
const catalog = projectWorkspaceModelCatalog(selections, {
|
|
637
|
+
defaultSelection,
|
|
638
|
+
creditsSelection: creditsDefaultSessionModel({
|
|
639
|
+
settings: workspaceCatalogSettings,
|
|
640
|
+
selections,
|
|
641
|
+
workspaceSettings,
|
|
642
|
+
}),
|
|
643
|
+
});
|
|
644
|
+
if (creditBalance) {
|
|
645
|
+
for (const model of catalog.models) {
|
|
646
|
+
if (model.cost !== "credits") continue;
|
|
647
|
+
model.creditFunding = creditBalance.promotionalCredits?.some(
|
|
648
|
+
(credit) => credit.remainingMicros > 0 && credit.eligibleModelIds.includes(model.id),
|
|
649
|
+
)
|
|
650
|
+
? "promotional"
|
|
651
|
+
: spendableCreditMicros(creditBalance, model.id) > 0
|
|
652
|
+
? "general"
|
|
653
|
+
: "unavailable";
|
|
654
|
+
}
|
|
655
|
+
}
|
|
630
656
|
c.header("cache-control", "private, no-store");
|
|
631
|
-
return c.json(
|
|
632
|
-
WorkspaceModelCatalogResponse.parse(
|
|
633
|
-
projectWorkspaceModelCatalog(selections, {
|
|
634
|
-
defaultSelection,
|
|
635
|
-
creditsSelection: creditsDefaultSessionModel({
|
|
636
|
-
settings: workspaceCatalogSettings,
|
|
637
|
-
selections,
|
|
638
|
-
workspaceSettings,
|
|
639
|
-
}),
|
|
640
|
-
}),
|
|
641
|
-
),
|
|
642
|
-
);
|
|
657
|
+
return c.json(WorkspaceModelCatalogResponse.parse(catalog));
|
|
643
658
|
});
|
|
644
659
|
|
|
645
660
|
app.get("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
|