referral-backfill.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { and, Database, eq, inArray, isNull } from "../src/drizzle/index.js"
  2. import { Identifier } from "../src/identifier.js"
  3. import { Referral } from "../src/referral.js"
  4. import { LiteTable } from "../src/schema/billing.sql.js"
  5. import { ReferralRewardTable, ReferralTable } from "../src/schema/referral.sql.js"
  6. import { UserTable } from "../src/schema/user.sql.js"
  7. import { WorkspaceTable } from "../src/schema/workspace.sql.js"
  8. const backfills = [
  9. {
  10. inviterWorkspaceID: "wrk_00000000000000000000000000",
  11. inviteeWorkspaceID: "wrk_00000000000000000000000000",
  12. inviteeAccountID: "acc_00000000000000000000000000",
  13. },
  14. ]
  15. console.log(`Backfilling ${backfills.length} referrals`)
  16. for (const [index, backfill] of backfills.entries()) {
  17. console.log(`[${index + 1}/${backfills.length}] ${backfill.inviterWorkspaceID} -> ${backfill.inviteeWorkspaceID}`)
  18. console.log(` invitee account: ${backfill.inviteeAccountID}`)
  19. const result = await Database.transaction(async (tx) => {
  20. if (backfill.inviterWorkspaceID === backfill.inviteeWorkspaceID) throw new Error("Self-referral workspace mismatch")
  21. const inviterWorkspace = await tx
  22. .select({ id: WorkspaceTable.id })
  23. .from(WorkspaceTable)
  24. .where(and(eq(WorkspaceTable.id, backfill.inviterWorkspaceID), isNull(WorkspaceTable.timeDeleted)))
  25. .then((rows) => rows[0])
  26. if (!inviterWorkspace) throw new Error(`Inviter workspace not found: ${backfill.inviterWorkspaceID}`)
  27. const inviteeWorkspace = await tx
  28. .select({ id: WorkspaceTable.id })
  29. .from(WorkspaceTable)
  30. .where(and(eq(WorkspaceTable.id, backfill.inviteeWorkspaceID), isNull(WorkspaceTable.timeDeleted)))
  31. .then((rows) => rows[0])
  32. if (!inviteeWorkspace) throw new Error(`Invitee workspace not found: ${backfill.inviteeWorkspaceID}`)
  33. const inviteeUser = await tx
  34. .select({ id: UserTable.id })
  35. .from(UserTable)
  36. .where(
  37. and(
  38. eq(UserTable.workspaceID, backfill.inviteeWorkspaceID),
  39. eq(UserTable.accountID, backfill.inviteeAccountID),
  40. eq(UserTable.role, "admin"),
  41. isNull(UserTable.timeDeleted),
  42. ),
  43. )
  44. .then((rows) => rows[0])
  45. if (!inviteeUser) throw new Error(`Invitee workspace owner not found: ${backfill.inviteeAccountID}`)
  46. const inviterUser = await tx
  47. .select({ id: UserTable.id })
  48. .from(UserTable)
  49. .where(
  50. and(
  51. eq(UserTable.workspaceID, backfill.inviterWorkspaceID),
  52. eq(UserTable.accountID, backfill.inviteeAccountID),
  53. isNull(UserTable.timeDeleted),
  54. ),
  55. )
  56. .then((rows) => rows[0])
  57. if (inviterUser) throw new Error(`Self-referral is not allowed: ${backfill.inviteeAccountID}`)
  58. const lite = await tx
  59. .select({ id: LiteTable.id })
  60. .from(LiteTable)
  61. .where(
  62. and(
  63. eq(LiteTable.workspaceID, backfill.inviteeWorkspaceID),
  64. eq(LiteTable.userID, inviteeUser.id),
  65. isNull(LiteTable.timeDeleted),
  66. ),
  67. )
  68. .then((rows) => rows[0])
  69. if (!lite) throw new Error(`Invitee Lite subscription not found: ${backfill.inviteeWorkspaceID}`)
  70. const existingReferral = await tx
  71. .select({ id: ReferralTable.id, workspaceID: ReferralTable.workspaceID })
  72. .from(ReferralTable)
  73. .where(and(eq(ReferralTable.inviteeAccountID, backfill.inviteeAccountID), isNull(ReferralTable.timeDeleted)))
  74. .then((rows) => rows[0])
  75. if (existingReferral && existingReferral.workspaceID !== backfill.inviterWorkspaceID) {
  76. throw new Error(`Referral already belongs to ${existingReferral.workspaceID}: ${existingReferral.id}`)
  77. }
  78. const referralID = existingReferral?.id ?? Identifier.create("referral")
  79. if (!existingReferral) {
  80. await tx.insert(ReferralTable).ignore().values({
  81. workspaceID: backfill.inviterWorkspaceID,
  82. id: referralID,
  83. inviteeAccountID: backfill.inviteeAccountID,
  84. })
  85. const referral = await tx
  86. .select({ id: ReferralTable.id })
  87. .from(ReferralTable)
  88. .where(and(eq(ReferralTable.inviteeAccountID, backfill.inviteeAccountID), isNull(ReferralTable.timeDeleted)))
  89. .then((rows) => rows[0])
  90. if (!referral) throw new Error(`Referral not created: ${backfill.inviteeAccountID}`)
  91. if (referral.id !== referralID) throw new Error(`Referral already redeemed: ${referral.id}`)
  92. }
  93. const rewardInsert = await tx
  94. .insert(ReferralRewardTable)
  95. .ignore()
  96. .values([
  97. {
  98. workspaceID: backfill.inviterWorkspaceID,
  99. referralID,
  100. amount: Referral.REWARD_AMOUNT,
  101. },
  102. {
  103. workspaceID: backfill.inviteeWorkspaceID,
  104. referralID,
  105. amount: Referral.REWARD_AMOUNT,
  106. },
  107. ])
  108. const rewards = await tx
  109. .select({ workspaceID: ReferralRewardTable.workspaceID, amount: ReferralRewardTable.amount })
  110. .from(ReferralRewardTable)
  111. .where(
  112. and(
  113. eq(ReferralRewardTable.referralID, referralID),
  114. inArray(ReferralRewardTable.workspaceID, [backfill.inviterWorkspaceID, backfill.inviteeWorkspaceID]),
  115. isNull(ReferralRewardTable.timeDeleted),
  116. ),
  117. )
  118. if (rewards.length !== 2) throw new Error(`Referral rewards not created: ${referralID}`)
  119. if (rewards.some((reward) => reward.amount !== Referral.REWARD_AMOUNT)) {
  120. throw new Error(`Referral reward amount mismatch: ${referralID}`)
  121. }
  122. return {
  123. referralID,
  124. createdReferral: !existingReferral,
  125. createdRewards: rewardInsert.rowsAffected,
  126. inviteeUserID: inviteeUser.id,
  127. liteID: lite.id,
  128. rewardWorkspaces: rewards.map((reward) => reward.workspaceID),
  129. }
  130. })
  131. console.log(` invitee user: ${result.inviteeUserID}`)
  132. console.log(` lite: ${result.liteID}`)
  133. console.log(` referral: ${result.referralID} (${result.createdReferral ? "created" : "existing"})`)
  134. console.log(` rewards: ${result.rewardWorkspaces.join(", ")} (${result.createdRewards} inserted)`)
  135. }
  136. console.log("Referral backfill complete")