Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a30a864
type fix
BilalG1 Aug 20, 2025
d6397fa
custom customer type
BilalG1 Aug 20, 2025
5e5c0ae
merge dev
BilalG1 Aug 20, 2025
d0e4e09
small fixes
BilalG1 Aug 20, 2025
1018ee5
fix test
BilalG1 Aug 20, 2025
553ffe0
fix issues
BilalG1 Aug 20, 2025
7e95b6b
revert import changes
BilalG1 Aug 20, 2025
da6f000
fix test
BilalG1 Aug 20, 2025
4b79dd1
offer and item pages, edits, deletes
BilalG1 Aug 20, 2025
4c546b8
fix lint
BilalG1 Aug 20, 2025
c22bf3c
small fixes
BilalG1 Aug 21, 2025
2b19be0
fix copy
BilalG1 Aug 21, 2025
03c06a2
payments test mode
BilalG1 Aug 21, 2025
19d1d52
revoke codes
BilalG1 Aug 21, 2025
582f876
fix test
BilalG1 Aug 21, 2025
8626012
stackable purchases
BilalG1 Aug 21, 2025
d5f0dd4
server only offers
BilalG1 Aug 21, 2025
82ff799
seed extra-admins, small fixes
BilalG1 Aug 21, 2025
eb7ee91
wip
BilalG1 Aug 22, 2025
1a3b465
updated schema
N2D4 Aug 22, 2025
40ff247
Include by default price
N2D4 Aug 22, 2025
4645d09
Combine subscriptions from DB and include-by-default
N2D4 Aug 22, 2025
3488427
wip updated schema
BilalG1 Aug 23, 2025
101c98a
schema fixes, ledger transactions
BilalG1 Aug 25, 2025
b27c7c8
merge dev
BilalG1 Aug 25, 2025
e537f6b
type fixes
BilalG1 Aug 25, 2025
2198b63
Separate Stripe account ID from schema
N2D4 Aug 25, 2025
9a5ac32
Merge branch 'payments-tx-ledger-algo' into payments-separate-stripe-…
N2D4 Aug 25, 2025
54bdf9a
Update AGENTS.md
N2D4 Aug 25, 2025
d10452d
Make CLAUDE.md an alias
N2D4 Aug 25, 2025
5e2b22a
fix tests and getSubscriptions
BilalG1 Aug 26, 2025
90c0b24
Merge remote-tracking branch 'origin/payments-separate-stripe-account…
BilalG1 Aug 26, 2025
dfdd231
type fix
BilalG1 Aug 26, 2025
8545274
fix stripe account info
BilalG1 Aug 26, 2025
59d375b
fix tests
BilalG1 Aug 26, 2025
4b7f823
fix tests
BilalG1 Aug 26, 2025
ee93553
fix tests
BilalG1 Aug 26, 2025
6e1a689
Merge dev into payments-tx-ledger-algo
N2D4 Aug 26, 2025
0420aa3
fix tests
BilalG1 Aug 26, 2025
f01efbb
Merge branch 'payments-tx-ledger-algo' of https://github.com/stack-au…
BilalG1 Aug 26, 2025
b36ed7e
fix createCheckoutUrl
BilalG1 Aug 26, 2025
157c024
payments fixes
BilalG1 Aug 27, 2025
7b2ac57
validate-code extra info
BilalG1 Aug 27, 2025
c878b3f
Merge branch 'dev' into payments-tx-ledger-algo
N2D4 Aug 27, 2025
dc37b10
fix test
BilalG1 Aug 27, 2025
2645635
fix conflicts
BilalG1 Aug 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
#### Extra commands
These commands are usually already called by the user, but you can remind them to run it for you if they forgot to.
- **Build packages**: `pnpm build:packages`
- **Generate code**: `pnpm codegen`
- **Start dependencies**: `pnpm restart-deps` (resets & restarts Docker containers for DB, Inbucket, etc. Usually already called by the user)
- **Run development**: `pnpm dev` (starts all services on different ports. Usually already started by the user in the background)
- **Run development**: Already called by the user in the background. You don't need to do this. This will also watch for changes and rebuild packages, codegen, etc.
- **Run minimal dev**: `pnpm dev:basic` (only backend and dashboard for resource-limited systems)

### Testing
Expand Down Expand Up @@ -69,15 +68,14 @@ The API follows a RESTful design with routes organized by resource type:
To see all development ports, refer to the index.html of `apps/dev-launchpad/public/index.html`.

## Important Notes
- NEVER UPDATE packages/stack OR packages/js. Instead, update packages/template, as the others are simply copies of that package.
- For blocking alerts and errors, never use `toast`, as they are easily missed by the user. Instead, use alerts.
- Environment variables are pre-configured in `.env.development` files
- Always run typecheck, lint, and test to make sure your changes are working as expected. You can save time by only linting and testing the files you've changed (and/or related E2E tests).
- The project uses a custom route handler system in the backend for consistent API responses
- Sometimes, the typecheck will give errors along the line of "Cannot assign Buffer to Uint8Array" or similar, on changes that are completely unrelated to your own changes. If that happens, stop and tell the user to run `pnpm clean && pnpm i && pnpm run codegen && pnpm build:packages`, and restart the dev server (you cannot run this yourself). After that's done, the typecheck should pass.
- Whenever you learn something new, or at the latest right before you call the `Stop` tool, write whatever you learned into the .claude/CLAUDE-KNOWLEDGE.md file, in the Q&A format in there. You will later be able to look up knowledge from there (based on the question you asked). Note that it's not 100% accurate and you may have to update it later if you find that something is wrong.
- Sometimes, the typecheck will give errors along the line of "Cannot assign Buffer to Uint8Array" or similar, on changes that are completely unrelated to your own changes. If that happens, tell the user to run `pnpm clean && pnpm i && pnpm run codegen && pnpm build:packages`, and restart the dev server (you cannot run this yourself). After that's done, the typecheck should pass.
- When writing tests, prefer .toMatchInlineSnapshot over other selectors, if possible. You can check (and modify) the snapshot-serializer.ts file to see how the snapshots are formatted and how non-deterministic values are handled.
- Whenever you learn something new, or at the latest right before you call the `Stop` tool, write whatever you learned into the ./claude/CLAUDE-KNOWLEDGE.md file, in the Q&A format in there. You will later be able to look up knowledge from there (based on the question you asked).

### Code-related
- Use ES6 maps instead of records wherever you can.
- Use `performance.now()` where appropriate for timing deltas, not `Date.now()`

### Testing-related
- When writing tests, prefer .toMatchInlineSnapshot over other matchers, if possible. You can check (and modify) the snapshot-serializer.ts file to see how the snapshots are formatted and how non-deterministic values are handled.
Binary file modified CLAUDE.md
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Subscription" ADD COLUMN "quantity" INTEGER NOT NULL DEFAULT 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Subscription" ADD COLUMN "offerId" TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "stripeAccountId" TEXT;
3 changes: 3 additions & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ model Project {
fullLogoUrl String?

projectConfigOverride Json?
stripeAccountId String?

apiKeySets ApiKeySet[]
projectUsers ProjectUser[]
Expand Down Expand Up @@ -739,7 +740,9 @@ model Subscription {
tenancyId String @db.Uuid
customerId String
customerType CustomerType
offerId String?
offer Json
quantity Int @default(1)

Comment thread
BilalG1 marked this conversation as resolved.
stripeSubscriptionId String?
status SubscriptionStatus
Expand Down
56 changes: 49 additions & 7 deletions apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,14 @@ async function seed() {
branchId: DEFAULT_BRANCH_ID,
environmentConfigOverrideOverride: {
payments: {
groups: {
plans: {
displayName: "Plans",
}
},
offers: {
team: {
groupId: "plans",
displayName: "Team",
customerType: "team",
serverOnly: false,
Expand All @@ -106,13 +112,14 @@ async function seed() {
},
includedItems: {
dashboard_admins: {
quantity: 2,
quantity: 3,
repeat: "never",
expires: "when-purchase-expires"
}
}
},
growth: {
groupId: "plans",
displayName: "Growth",
customerType: "team",
serverOnly: false,
Expand All @@ -126,21 +133,56 @@ async function seed() {
},
includedItems: {
dashboard_admins: {
quantity: 4,
quantity: 5,
repeat: "never",
expires: "when-purchase-expires"
}
}
},
free: {
groupId: "plans",
displayName: "Free",
customerType: "team",
serverOnly: false,
stackable: false,
prices: "include-by-default",
includedItems: {
dashboard_admins: {
quantity: 1,
repeat: "never",
expires: "when-purchase-expires"
}
}
},
"extra-admins": {
groupId: "plans",
displayName: "Extra Admins",
customerType: "team",
serverOnly: false,
stackable: true,
prices: {
monthly: {
USD: "49",
interval: [1, "month"] as any,
serverOnly: false
}
},
includedItems: {
dashboard_admins: {
quantity: 1,
repeat: "never",
expires: "when-purchase-expires"
}
},
isAddOnTo: {
team: true,
growth: true,
}
}
},
items: {
dashboard_admins: {
displayName: "Dashboard Admins",
default: {
quantity: 1,
expires: "never",
repeat: "never"
},
customerType: "team"
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getStackStripe, syncStripeAccountStatus, syncStripeSubscriptions } from "@/lib/stripe";
import { getStackStripe, syncStripeSubscriptions } from "@/lib/stripe";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { yupMixed, yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
Expand Down Expand Up @@ -65,7 +65,6 @@ export const POST = createSmartRouteHandler({
if (!event.account) {
throw new StackAssertionError("Stripe webhook account id missing", { event });
}
await syncStripeAccountStatus(event.account);
} else if (isSubscriptionChangedEvent(event)) {
const accountId = event.account;
const customerId = (event.data.object as any).customer;
Expand Down
20 changes: 12 additions & 8 deletions apps/backend/src/app/api/latest/internal/payments/setup/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { overrideEnvironmentConfigOverride } from "@/lib/config";
import { getStackStripe } from "@/lib/stripe";
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, adminAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
Expand All @@ -24,7 +24,13 @@ export const POST = createSmartRouteHandler({
}),
handler: async ({ auth }) => {
const stripe = getStackStripe();
let stripeAccountId = auth.tenancy.config.payments.stripeAccountId;

const project = await globalPrismaClient.project.findUnique({
where: { id: auth.project.id },
select: { stripeAccountId: true },
});

let stripeAccountId = project?.stripeAccountId || null;
const returnToUrl = new URL(`/projects/${auth.project.id}/payments`, getEnvVariable("NEXT_PUBLIC_STACK_DASHBOARD_URL")).toString();

if (!stripeAccountId) {
Expand All @@ -42,12 +48,10 @@ export const POST = createSmartRouteHandler({
}
});
stripeAccountId = account.id;
await overrideEnvironmentConfigOverride({
projectId: auth.project.id,
branchId: auth.tenancy.branchId,
environmentConfigOverrideOverride: {
[`payments.stripeAccountId`]: stripeAccountId,
},

await globalPrismaClient.project.update({
where: { id: auth.project.id },
data: { stripeAccountId },
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getStackStripe } from "@/lib/stripe";
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, adminAuthTypeSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
Expand All @@ -23,12 +24,18 @@ export const POST = createSmartRouteHandler({
}),
handler: async ({ auth }) => {
const stripe = getStackStripe();
if (!auth.tenancy.config.payments.stripeAccountId) {

const project = await globalPrismaClient.project.findUnique({
where: { id: auth.project.id },
select: { stripeAccountId: true },
});

if (!project?.stripeAccountId) {
throw new StatusError(400, "Stripe account ID is not set");
}

const accountSession = await stripe.accountSessions.create({
account: auth.tenancy.config.payments.stripeAccountId,
account: project.stripeAccountId,
components: {
payments: {
enabled: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { getStackStripe } from "@/lib/stripe";
import { globalPrismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, adminAuthTypeSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";

export const GET = createSmartRouteHandler({
metadata: {
hidden: true,
},
request: yupObject({
auth: yupObject({
type: adminAuthTypeSchema.defined(),
project: adaptSchema.defined(),
tenancy: adaptSchema.defined(),
}).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
account_id: yupString().defined(),
charges_enabled: yupBoolean().defined(),
details_submitted: yupBoolean().defined(),
payouts_enabled: yupBoolean().defined(),
}).nullable(),
}),
handler: async ({ auth }) => {
const project = await globalPrismaClient.project.findUnique({
where: { id: auth.project.id },
select: { stripeAccountId: true },
});

if (!project?.stripeAccountId) {
return {
statusCode: 200,
bodyType: "json",
body: null,
};
}

const stripe = getStackStripe();
const account = await stripe.accounts.retrieve(project.stripeAccountId);

return {
statusCode: 200,
bodyType: "json",
body: {
account_id: account.id,
charges_enabled: account.charges_enabled || false,
details_submitted: account.details_submitted || false,
payouts_enabled: account.payouts_enabled || false,
},
};
Comment thread
BilalG1 marked this conversation as resolved.
},
});
Loading