core: let users apply Go referral credit by invite

Users can preview and apply available Go referral credit from the referral itself, avoiding hidden reward ids that do not map to the invite they see.
This commit is contained in:
vimtor
2026-05-16 13:19:16 +02:00
parent 50d1e78e34
commit 95ba577528
6 changed files with 3112 additions and 23 deletions

View File

@@ -70,9 +70,9 @@ export const queryGoReferral = query(async (workspaceID: string) => {
}, workspaceID)
}, "go.referral.get")
export const queryGoReferralUsagePreview = query(async (workspaceID: string, rewardID?: string) => {
export const queryGoReferralUsagePreview = query(async (workspaceID: string, referralID?: string) => {
"use server"
if (!rewardID) return null
if (!referralID) return null
return withActor(async () => {
const row = await Database.use((tx) =>
tx
@@ -91,7 +91,7 @@ export const queryGoReferralUsagePreview = query(async (workspaceID: string, rew
.where(
and(
eq(ReferralRewardTable.workspaceID, workspaceID),
eq(ReferralRewardTable.id, rewardID),
eq(ReferralRewardTable.referralID, referralID),
isNull(ReferralRewardTable.timeApplied),
isNull(ReferralRewardTable.timeDeleted),
isNull(LiteTable.timeDeleted),
@@ -145,12 +145,12 @@ export const queryGoReferralUsagePreview = query(async (workspaceID: string, rew
}, workspaceID)
}, "go.referral.usagePreview")
export const applyGoReferralReward = action(async (workspaceID: string, rewardID: string) => {
export const applyGoReferralReward = action(async (workspaceID: string, referralID: string) => {
"use server"
return json(
await withActor(
() =>
Referral.applyReward({ rewardID })
Referral.applyReward({ referralID })
.then((data) => ({ error: undefined, data }))
.catch((e) => ({ error: e.message as string, data: undefined })),
workspaceID,

View File

@@ -0,0 +1,4 @@
DROP INDEX `referral_reward_workspace_time` ON `referral_reward`;--> statement-breakpoint
ALTER TABLE `referral_reward` DROP PRIMARY KEY;--> statement-breakpoint
ALTER TABLE `referral_reward` ADD PRIMARY KEY (`workspace_id`,`referral_id`);--> statement-breakpoint
ALTER TABLE `referral_reward` DROP COLUMN `id`;

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,6 @@ export namespace Identifier {
payment: "pay",
provider: "prv",
referral: "ref",
referralReward: "rrw",
subscription: "sub",
usage: "usg",
user: "usr",

View File

@@ -61,7 +61,6 @@ export namespace Referral {
const rows = await Database.use(async (tx) => {
const rewards = await tx
.select({
id: ReferralRewardTable.id,
referralID: ReferralRewardTable.referralID,
workspaceID: ReferralRewardTable.workspaceID,
referralWorkspaceID: ReferralTable.workspaceID,
@@ -114,7 +113,7 @@ export namespace Referral {
const rewardReferralIDs = new Set(rows.rewards.map((reward) => reward.referralID))
const inviteeRewardReferralIDs = new Set(rows.inviteeRewards.map((reward) => reward.referralID))
const rewards = rows.rewards.map((reward) => ({
id: reward.id,
id: reward.referralID,
source: reward.workspaceID === reward.referralWorkspaceID ? ("inviter" as const) : ("invitee" as const),
status: reward.timeApplied ? ("applied" as const) : ("available" as const),
amount: microCentsToCents(reward.amount),
@@ -158,17 +157,17 @@ export namespace Referral {
}
})
export const applyReward = fn(z.object({ rewardID: z.string() }), async (input) => {
export const applyReward = fn(z.object({ referralID: z.string() }), async (input) => {
const workspaceID = Actor.workspace()
return Database.transaction(async (tx) => {
const reward = await tx
.select({ id: ReferralRewardTable.id, amount: ReferralRewardTable.amount, timeApplied: ReferralRewardTable.timeApplied })
.select({ amount: ReferralRewardTable.amount, timeApplied: ReferralRewardTable.timeApplied })
.from(ReferralRewardTable)
.where(
and(
eq(ReferralRewardTable.workspaceID, workspaceID),
eq(ReferralRewardTable.id, input.rewardID),
eq(ReferralRewardTable.referralID, input.referralID),
isNull(ReferralRewardTable.timeDeleted),
),
)
@@ -191,7 +190,7 @@ export namespace Referral {
.where(
and(
eq(ReferralRewardTable.workspaceID, workspaceID),
eq(ReferralRewardTable.id, input.rewardID),
eq(ReferralRewardTable.referralID, input.referralID),
isNull(ReferralRewardTable.timeApplied),
isNull(ReferralRewardTable.timeDeleted),
),
@@ -312,7 +311,7 @@ export namespace Referral {
if (!referral) return { status: "missing-referral" as const }
const existingRewards = await tx
.select({ id: ReferralRewardTable.id })
.select({ referralID: ReferralRewardTable.referralID })
.from(ReferralRewardTable)
.where(and(eq(ReferralRewardTable.referralID, referral.id), isNull(ReferralRewardTable.timeDeleted)))
if (existingRewards.length > 0) return { status: "already-completed" as const }
@@ -322,13 +321,11 @@ export namespace Referral {
.values([
{
workspaceID: referral.workspaceID,
id: Identifier.create("referralReward"),
referralID: referral.id,
amount: REWARD_AMOUNT,
},
{
workspaceID: input.workspaceID,
id: Identifier.create("referralReward"),
referralID: referral.id,
amount: REWARD_AMOUNT,
},

View File

@@ -1,29 +1,28 @@
import { bigint, index, mysqlTable, uniqueIndex } from "drizzle-orm/mysql-core"
import { timestamps, ulid, utc } from "../drizzle/types"
import { bigint, mysqlTable, primaryKey, uniqueIndex } from "drizzle-orm/mysql-core"
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
import { workspaceIndexes } from "./workspace.sql"
export const ReferralTable = mysqlTable(
"referral",
{
id: ulid("id").notNull().primaryKey(),
workspaceID: ulid("workspace_id").notNull(),
...workspaceColumns,
...timestamps,
inviteeAccountID: ulid("invitee_account_id").notNull(),
},
(table) => [
...workspaceIndexes(table),
uniqueIndex("referral_invitee_account_id").on(table.inviteeAccountID),
index("referral_workspace_id").on(table.workspaceID),
],
)
export const ReferralRewardTable = mysqlTable(
"referral_reward",
{
id: ulid("id").notNull().primaryKey(),
workspaceID: ulid("workspace_id"),
workspaceID: ulid("workspace_id").notNull(),
...timestamps,
referralID: ulid("referral_id").notNull(),
amount: bigint("amount", { mode: "number" }).notNull(),
timeApplied: utc("time_applied"),
},
(table) => [index("referral_reward_workspace_time").on(table.workspaceID, table.timeCreated)],
(table) => [primaryKey({ columns: [table.workspaceID, table.referralID] })],
)