Skip to content

Commit 558ff97

Browse files
nfmelendezcruzdanilo
authored andcommitted
✨ server: support multiple cards in statement
1 parent 1e5616a commit 558ff97

5 files changed

Lines changed: 381 additions & 283 deletions

File tree

.changeset/soft-trains-dig.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@exactly/server": patch
3+
---
4+
5+
✨ support multiple cards in statement

server/api/activity.ts

Lines changed: 48 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { renderToBuffer } from "@react-pdf/renderer";
22

33
import { captureException, setUser } from "@sentry/node";
4-
import { and, arrayOverlaps, eq, inArray } from "drizzle-orm";
4+
import { arrayOverlaps, eq } from "drizzle-orm";
55
import { Hono } from "hono";
66
import { accepts } from "hono/accepts";
77
import { validator as vValidator } from "hono-openapi/valibot";
@@ -90,7 +90,7 @@ export default new Hono().get(
9090
columns: { account: true },
9191
with: {
9292
cards: {
93-
columns: {},
93+
columns: { id: true, lastFour: true },
9494
with: { transactions: { columns: { hashes: true, payload: true } } },
9595
limit: ignore("card") || maturity !== undefined ? 0 : undefined,
9696
},
@@ -262,29 +262,26 @@ export default new Hono().get(
262262
].map((blockNumber) => publicClient.getBlock({ blockNumber })),
263263
);
264264
const timestamps = new Map(blocks.map(({ number: block, timestamp }) => [block, timestamp]));
265-
let statementCards: string[] = [];
266-
let cardPurchases: typeof credential.cards;
267-
if (!ignore("card") && maturity !== undefined && borrows) {
268-
const hashes = borrows
269-
.entries()
270-
.filter(([_, { events }]) => events.some(({ maturity: m }) => Number(m) === maturity))
271-
.map(([hash]) => hash)
272-
.toArray();
273-
const userCards = await database.query.cards
274-
.findMany({ columns: { id: true }, where: eq(cards.credentialId, credentialId) })
275-
.then((rows) => rows.map(({ id }) => id));
276-
const statementTransactions =
277-
hashes.length === 0 || userCards.length === 0
278-
? []
279-
: await database.query.transactions.findMany({
280-
where: and(arrayOverlaps(transactions.hashes, hashes), inArray(transactions.cardId, userCards)),
281-
columns: { cardId: true, hashes: true, payload: true },
265+
const purchases =
266+
!ignore("card") && borrows && maturity !== undefined
267+
? await (() => {
268+
const hashes = borrows
269+
.entries()
270+
.filter(([_, { events }]) => events.some(({ maturity: m }) => Number(m) === maturity))
271+
.map(([hash]) => hash)
272+
.toArray();
273+
return database.query.cards.findMany({
274+
where: eq(cards.credentialId, credentialId),
275+
columns: { id: true, lastFour: true },
276+
with: {
277+
transactions: {
278+
columns: { hashes: true, payload: true },
279+
where: arrayOverlaps(transactions.hashes, hashes),
280+
},
281+
},
282282
});
283-
statementCards = [...new Set(statementTransactions.map(({ cardId }) => cardId))];
284-
cardPurchases = [{ transactions: statementTransactions }];
285-
} else {
286-
cardPurchases = credential.cards;
287-
}
283+
})()
284+
: credential.cards;
288285

289286
const accept = accepts(c, {
290287
header: "Accept",
@@ -294,7 +291,7 @@ export default new Hono().get(
294291
const pdf = accept === "application/pdf";
295292

296293
const response = [
297-
...cardPurchases.flatMap(({ transactions: txs }) =>
294+
...purchases.flatMap(({ transactions: txs }) =>
298295
txs.map(({ hashes, payload }) => {
299296
const panda = safeParse(PandaActivity, {
300297
...(payload as object),
@@ -423,20 +420,16 @@ export default new Hono().get(
423420
.toSorted((a, b) => b.timestamp.localeCompare(a.timestamp) || b.id.localeCompare(a.id));
424421

425422
if (maturity !== undefined && pdf) {
426-
if (statementCards.length > 1) return c.json({ code: "multiple cards" }, 400);
427-
const statementCurrency = market(marketUSDCAddress).symbol;
428-
const card =
429-
statementCards.length === 0
430-
? undefined
431-
: await database.query.cards.findFirst({
432-
columns: { lastFour: true },
433-
where: and(eq(cards.credentialId, credentialId), inArray(cards.id, statementCards)),
434-
});
435-
const statement = {
436-
maturity,
437-
lastFour: card?.lastFour ?? "",
438-
data: response.flatMap((item): Parameters<typeof Statement>[0]["data"] => {
423+
const cardLookup = new Map(
424+
purchases.flatMap(({ id, transactions: txs }) =>
425+
txs.flatMap(({ hashes }) => hashes.map((hash) => [hash, id] as const)),
426+
),
427+
);
428+
const purchasesByCard = Map.groupBy(
429+
response.flatMap((item) => {
439430
if (item.type === "panda") {
431+
const cardId = item.operations[0] && cardLookup.get(item.operations[0].transactionHash);
432+
if (!cardId) return [];
440433
const installments = item.operations
441434
.reduce((accumulator, operation) => {
442435
if ("borrow" in operation) {
@@ -470,6 +463,7 @@ export default new Hono().get(
470463
if (installments.length === 0) return [];
471464
return [
472465
{
466+
cardId,
473467
id: item.id,
474468
timestamp: item.timestamp,
475469
description: `${item.merchant.name}${item.merchant.city ? `, ${item.merchant.city}` : ""}`,
@@ -478,6 +472,8 @@ export default new Hono().get(
478472
];
479473
}
480474
if (item.type === "card" && "borrow" in item) {
475+
const cardId = cardLookup.get(item.transactionHash);
476+
if (!cardId) return [];
481477
if ("installments" in item.borrow) {
482478
const events = borrows?.get(item.transactionHash)?.events;
483479
if (!events) return [];
@@ -490,6 +486,7 @@ export default new Hono().get(
490486
if (installments.length === 0) return [];
491487
return [
492488
{
489+
cardId,
493490
id: item.id,
494491
timestamp: item.timestamp,
495492
description: `${item.merchant.name}${item.merchant.city ? `, ${item.merchant.city}` : ""}`,
@@ -501,27 +498,28 @@ export default new Hono().get(
501498
if (!borrow || Number(borrow.maturity) !== maturity) return [];
502499
return [
503500
{
501+
cardId,
504502
id: item.id,
505503
timestamp: item.timestamp,
506504
description: `${item.merchant.name}${item.merchant.city ? `, ${item.merchant.city}` : ""}`,
507505
installments: [{ amount: Number(borrow.assets + borrow.fee) / 1e6, current: 1, total: 1 }],
508506
},
509507
];
510508
}
511-
if (item.type === "repay") {
512-
if (item.currency !== statementCurrency) return [];
513-
return [
514-
{
515-
id: item.id,
516-
timestamp: item.timestamp,
517-
currency: item.currency,
518-
positionAmount: item.positionAmount,
519-
amount: item.amount,
520-
},
521-
];
522-
}
523509
return [];
524510
}),
511+
({ cardId }) => cardId,
512+
);
513+
const statement = {
514+
account: `${account.slice(0, 6)}...${account.slice(-6)}`,
515+
maturity,
516+
cards: purchases.map(({ id, lastFour }) => ({
517+
lastFour,
518+
purchases: (purchasesByCard.get(id) ?? []).map(({ cardId: _, ...rest }) => rest),
519+
})),
520+
payments: response
521+
.filter(({ type, currency }) => type === "repay" && currency === market(marketUSDCAddress).symbol)
522+
.map(({ id, timestamp, amount }) => ({ id, timestamp, amount })),
525523
};
526524
return c.body(new Uint8Array(await renderToBuffer(Statement(statement))), 200, {
527525
"content-type": "application/pdf",

server/test/api/activity.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ describe.concurrent("authenticated", () => {
6969
let maturity: string;
7070

7171
beforeAll(async () => {
72-
await database.insert(cards).values([{ id: "activity", credentialId: "bob", lastFour: "1234" }]);
72+
await database.insert(cards).values([
73+
{ id: "first-activity-card", credentialId: "bob", lastFour: "1234" },
74+
{ id: "second-activity-card", credentialId: "bob", lastFour: "6789" },
75+
]);
7376
const borrows = await anvilClient.getContractEvents({
7477
abi: marketAbi,
7578
eventName: "BorrowAtMaturity",
@@ -148,7 +151,7 @@ describe.concurrent("authenticated", () => {
148151
};
149152
return {
150153
id: String(index),
151-
cardId: "activity",
154+
cardId: index === 0 ? "first-activity-card" : "second-activity-card",
152155
hashes,
153156
payload,
154157
hash,
@@ -218,7 +221,7 @@ describe.concurrent("authenticated", () => {
218221
it("reports bad transaction", async () => {
219222
await database
220223
.insert(transactions)
221-
.values([{ id: "bad-transaction", cardId: "activity", hashes: ["0x1"], payload: {} }]);
224+
.values([{ id: "bad-transaction", cardId: "first-activity-card", hashes: ["0x1"], payload: {} }]);
222225
const response = await appClient.index.$get(
223226
{ query: { include: "card" } },
224227
{ headers: { "test-credential-id": "bob" } },

0 commit comments

Comments
 (0)