-
Notifications
You must be signed in to change notification settings - Fork 2
✨ server: add pax for platinum to signature card upgrades #670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@exactly/server": patch | ||
| --- | ||
|
|
||
| ✨ add pax for platinum to signature card upgrades |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||
| import { captureException, setContext, setUser } from "@sentry/node"; | ||||||||||||||
| import { Mutex } from "async-mutex"; | ||||||||||||||
| import { eq, inArray, ne } from "drizzle-orm"; | ||||||||||||||
| import { and, eq, inArray, ne } from "drizzle-orm"; | ||||||||||||||
| import { Hono } from "hono"; | ||||||||||||||
| import { describeRoute } from "hono-openapi"; | ||||||||||||||
| import { resolver, validator as vValidator } from "hono-openapi/valibot"; | ||||||||||||||
|
|
@@ -25,13 +25,15 @@ import { | |||||||||||||
| } from "valibot"; | ||||||||||||||
|
|
||||||||||||||
| import MAX_INSTALLMENTS from "@exactly/common/MAX_INSTALLMENTS"; | ||||||||||||||
| import { SIGNATURE_PRODUCT_ID } from "@exactly/common/panda"; | ||||||||||||||
| import { PLATINUM_PRODUCT_ID, SIGNATURE_PRODUCT_ID } from "@exactly/common/panda"; | ||||||||||||||
| import { Address } from "@exactly/common/validation"; | ||||||||||||||
|
|
||||||||||||||
| import database, { cards, credentials } from "../database"; | ||||||||||||||
| import auth from "../middleware/auth"; | ||||||||||||||
| import { sendPushNotification } from "../utils/onesignal"; | ||||||||||||||
| import { autoCredit, createCard, getCard, getPIN, getSecrets, getUser, setPIN, updateCard } from "../utils/panda"; | ||||||||||||||
| import { addCapita, deriveAssociateId } from "../utils/pax"; | ||||||||||||||
| import { getAccount } from "../utils/persona"; | ||||||||||||||
| import { customer } from "../utils/sardine"; | ||||||||||||||
| import { track } from "../utils/segment"; | ||||||||||||||
| import validatorHook from "../utils/validatorHook"; | ||||||||||||||
|
|
@@ -336,6 +338,17 @@ function decrypt(base64Secret: string, base64Iv: string, secretKey: string): str | |||||||||||||
| const account = parse(Address, credential.account); | ||||||||||||||
| setUser({ id: account }); | ||||||||||||||
|
|
||||||||||||||
| // check if this is an upgrade from platinum to signature | ||||||||||||||
| const deletedPlatinumCard = await database.query.cards.findFirst({ | ||||||||||||||
| where: and( | ||||||||||||||
| eq(cards.credentialId, credentialId), | ||||||||||||||
| eq(cards.status, "DELETED"), | ||||||||||||||
| eq(cards.productId, PLATINUM_PRODUCT_ID), | ||||||||||||||
| ), | ||||||||||||||
| columns: { id: true }, | ||||||||||||||
| }); | ||||||||||||||
| const isUpgradeFromPlatinum = !!deletedPlatinumCard; | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+342
to
+351
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: A user can be enrolled in Pax multiple times by repeatedly creating and deleting a signature card, as the upgrade check doesn't track previous enrollments. Suggested FixTo prevent duplicate enrollments, introduce a mechanism to track whether a user has already received the Pax upgrade benefit. This could be a new flag in the user's profile or a separate table that logs Pax enrollments. Before calling Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||||||||||||||
| if (!credential.pandaId) return c.json({ code: "no panda", legacy: "panda id not found" }, 403); | ||||||||||||||
| let cardCount = credential.cards.length; | ||||||||||||||
| for (const card of credential.cards) { | ||||||||||||||
|
|
@@ -367,6 +380,10 @@ function decrypt(base64Secret: string, base64Iv: string, secretKey: string): str | |||||||||||||
| .values([{ id: card.id, credentialId, lastFour: card.last4, mode, productId: SIGNATURE_PRODUCT_ID }]); | ||||||||||||||
| track({ event: "CardIssued", userId: account, properties: { productId: SIGNATURE_PRODUCT_ID } }); | ||||||||||||||
|
|
||||||||||||||
| if (isUpgradeFromPlatinum) { | ||||||||||||||
| addCapitaForPlatinumUpgrade(credentialId, account); | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+383
to
+385
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider using The fire-and-forget pattern is appropriate here to avoid blocking card creation. However, prepending ♻️ Suggested change if (isUpgradeFromPlatinum) {
- addCapitaForPlatinumUpgrade(credentialId, account);
+ void addCapitaForPlatinumUpgrade(credentialId, account);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| customer({ | ||||||||||||||
| flow: { name: "card.issued", type: "payment_method_link" }, | ||||||||||||||
| customer: { id: credentialId, type: "customer" }, | ||||||||||||||
|
|
@@ -445,12 +462,12 @@ async function encryptPIN(pin: string) { | |||||||||||||
| secretKeyBase64Buffer, | ||||||||||||||
| ); | ||||||||||||||
| const sessionId = secretKeyBase64BufferEncrypted.toString("base64"); | ||||||||||||||
|
|
||||||||||||||
| const iv = crypto.randomBytes(12); | ||||||||||||||
| const cipher = crypto.createCipheriv("aes-128-gcm", Buffer.from(secret, "hex"), iv); | ||||||||||||||
| const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]); | ||||||||||||||
| const authTag = cipher.getAuthTag(); | ||||||||||||||
|
|
||||||||||||||
| return { | ||||||||||||||
| data: Buffer.concat([encrypted, authTag]).toString("base64"), | ||||||||||||||
| iv: iv.toString("base64"), | ||||||||||||||
|
|
@@ -565,3 +582,30 @@ function buildBaseResponse(example = "string") { | |||||||||||||
| legacy: pipe(string(), metadata({ examples: [example] })), | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| function addCapitaForPlatinumUpgrade(credentialId: string, account: InferOutput<typeof Address>) { | ||||||||||||||
| getAccount(credentialId, "basic") | ||||||||||||||
| .then((personaAccount) => { | ||||||||||||||
| if (!personaAccount) throw new Error("no persona account found"); | ||||||||||||||
| const attributes = personaAccount.attributes; | ||||||||||||||
| const documents = attributes.fields.documents.value; | ||||||||||||||
| if (!documents[0]) throw new Error("no identity document found"); | ||||||||||||||
|
|
||||||||||||||
| return addCapita({ | ||||||||||||||
| firstName: attributes["name-first"], | ||||||||||||||
| lastName: attributes["name-last"], | ||||||||||||||
| birthdate: attributes.birthdate, | ||||||||||||||
| document: documents[0].value.id_number.value, | ||||||||||||||
| email: attributes["email-address"], | ||||||||||||||
| phone: attributes["phone-number"], | ||||||||||||||
| internalId: deriveAssociateId(account), | ||||||||||||||
| product: "travel insurance", | ||||||||||||||
| }); | ||||||||||||||
| }) | ||||||||||||||
| .catch((error: unknown) => { | ||||||||||||||
| captureException(error, { | ||||||||||||||
| level: "error", | ||||||||||||||
| extra: { credentialId, account, productId: SIGNATURE_PRODUCT_ID, scope: "basic" }, | ||||||||||||||
| }); | ||||||||||||||
| }); | ||||||||||||||
| } | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Pax enrollment missed for platinum users upgrading via migration path
Users whose platinum card gets deleted during the migration process will not be enrolled in pax travel insurance, even though they are effectively upgrading from platinum to signature.
Click to expand
Root Cause
The
isUpgradeFromPlatinumcheck at lines 342-350 queries for cards withstatus: "DELETED"andproductId: PLATINUM_PRODUCT_IDbefore the migration loop at lines 354-369 runs. The migration loop marks cards asDELETEDif they don't exist in Panda.Scenario
deletedPlatinumCardquery runs and finds nothing (card is still ACTIVE)isUpgradeFromPlatinumis set tofalseisUpgradeFromPlatinumisfalseActual vs Expected
Impact
Users who had platinum cards that were migrated/deleted during the upgrade process miss out on the travel insurance benefit they should receive.
Recommendation: Move the
isUpgradeFromPlatinumcheck to after the migration loop, or track which cards get deleted during migration and check if any of them were platinum cards. For example:Note: This also requires adding
productIdto the cards query at line 334.Was this helpful? React with 👍 or 👎 to provide feedback.