diff --git a/.bruno/Payment/account/create.bru b/.bruno/Payment/account/create.bru index 4dd2ca3..7a4bf7a 100644 --- a/.bruno/Payment/account/create.bru +++ b/.bruno/Payment/account/create.bru @@ -12,7 +12,7 @@ post { body:json { { - "ledgerId": "3c71d240-a375-42e1-9a78-0575bf33fabb", + "ledgerId": "ee228d0d-b48c-4878-aca3-050a7434503b", "entityId": "some-external-entity", "label": "Securities" } diff --git a/.bruno/Payment/beneficiary/Ledgers.bru b/.bruno/Payment/beneficiary/Ledgers.bru index 8eaacb6..3297d88 100644 --- a/.bruno/Payment/beneficiary/Ledgers.bru +++ b/.bruno/Payment/beneficiary/Ledgers.bru @@ -11,7 +11,7 @@ get { } params:path { - id: a0a6aa39-5d13-4717-9554-a878d7f30ea7 + id: 16f41847-4bc4-4898-92d1-75fd314d15a8 } settings { diff --git a/.bruno/Payment/ledger/create.bru b/.bruno/Payment/ledger/create.bru index fabd92a..27bd308 100644 --- a/.bruno/Payment/ledger/create.bru +++ b/.bruno/Payment/ledger/create.bru @@ -12,8 +12,8 @@ post { body:json { { - "beneficiaryId": "2f6dfb20-7834-484c-8472-096f72fc5f08", - "label": "Sample Ledger", + "beneficiaryId": "16f41847-4bc4-4898-92d1-75fd314d15a8", + "label": "Sample Ledger #1", "currencies": [ "NOK", "SEK" diff --git a/.bruno/Payment/wallet/create.bru b/.bruno/Payment/wallet/create.bru index 16af2fa..5d5e834 100644 --- a/.bruno/Payment/wallet/create.bru +++ b/.bruno/Payment/wallet/create.bru @@ -12,9 +12,9 @@ post { body:json { { - "walletId": "56f2aba8-5687-4e63-8d6a-e120b50ef891", + "walletId": "c13bd907-d760-4628-95e8-a723a54dff83", "currency": "NOK", - "label": "NOK Savings" + "label": "Sample Funds" } } diff --git a/modules/payment/repositories/account.ts b/modules/payment/repositories/account.ts index 8bcf99e..6bc3ba5 100644 --- a/modules/payment/repositories/account.ts +++ b/modules/payment/repositories/account.ts @@ -1,9 +1,7 @@ import { db } from "@platform/database"; -import { BadRequestError, ConflictError } from "@platform/relay"; +import { BadRequestError } from "@platform/relay"; import { type Account, type AccountInsert, AccountInsertSchema, AccountSchema } from "../schemas/account.ts"; -import { getLedgerById } from "./ledger.ts"; -import { getWalletById } from "./wallet.ts"; /** * Create a new account. @@ -15,23 +13,6 @@ export async function createAccount(values: AccountInsert): Promise { .begin(async () => { const _id = crypto.randomUUID(); - // const wallet = await getWalletById(values.walletId); - // if (wallet === undefined) { - // throw new ConflictError(`Wallet '${values.walletId}' does not exist`); - // } - - // const ledger = await getLedgerById(wallet.ledgerId); - // if (ledger === undefined) { - // // TODO: RAISE ALARMS, THIS SHOULD NEVER OCCUR - // throw new ConflictError(`Wallet ledger '${wallet.ledgerId}' does not exist`); - // } - - // if (ledger.currencies.includes(values.currency) === false) { - // throw new BadRequestError( - // `Ledger does not support '${values.currency}' currency, supported currencies '${ledger.currencies.join(", ")}'`, - // ); - // } - // Assert wallet exists await db.sql` ASSERT EXISTS ( @@ -58,11 +39,16 @@ export async function createAccount(values: AccountInsert): Promise { FROM payment.wallet w JOIN payment.ledger l ON l._id = w."ledgerId" WHERE w._id = ${db.text(values.walletId)} - AND ${db.text(values.currency)} = ANY(l.currencies) + AND ${db.text(values.currency)} IN ( + SELECT + currency + FROM + UNNEST(l.currencies) AS x(currency) + ) ), 'unsupported_currency'; `; - await db.sql`INSERT INTO payment.wallet RECORDS ${db.transit({ _id, ...AccountInsertSchema.parse(values) })}`; + await db.sql`INSERT INTO payment.account RECORDS ${db.transit({ _id, ...AccountInsertSchema.parse(values) })}`; return _id; }) @@ -72,13 +58,13 @@ export async function createAccount(values: AccountInsert): Promise { } switch (error.message) { case "missing_wallet": { - throw new ConflictError("Account wallet does not exist"); + throw new BadRequestError("Account wallet does not exist"); } case "missing_ledger": { - throw new ConflictError("Account ledger does not exist"); + throw new BadRequestError("Account ledger does not exist"); } case "unsupported_currency": { - throw new ConflictError("Invalid account currency"); + throw new BadRequestError("Invalid account currency"); } } throw error; diff --git a/modules/payment/repositories/ledger.ts b/modules/payment/repositories/ledger.ts index c96ed42..8fb30e5 100644 --- a/modules/payment/repositories/ledger.ts +++ b/modules/payment/repositories/ledger.ts @@ -1,7 +1,7 @@ import { db } from "@platform/database"; -import { ConflictError } from "@platform/relay"; +import { BadRequestError } from "@platform/relay"; -import { type Ledger, type LedgerInsert, LedgerInsertSchema, LedgerSchema } from "../schemas/ledger.ts"; +import { type Ledger, type LedgerInsert, LedgerSchema } from "../schemas/ledger.ts"; /** * Create a new ledger. @@ -13,28 +13,30 @@ export async function createLedger(values: LedgerInsert): Promise { .begin(async () => { const _id = crypto.randomUUID(); await db.sql`ASSERT EXISTS (SELECT 1 FROM payment.beneficiary WHERE _id = ${db.text(values.beneficiaryId)}), 'missing_beneficiary'`; - await db.sql`INSERT INTO payment.ledger RECORDS ${db.transit({ _id, ...LedgerInsertSchema.parse(values) })}`; + await db.sql` + INSERT INTO payment.ledger ( + _id, + "beneficiaryId", + currencies, + label + ) VALUES ( + ${db.text(_id)}, + ${db.text(values.beneficiaryId)}, + ${db.array(values.currencies)}, + ${values.label ? db.text(values.label) : null} + ) + `; return _id; }) .catch((error) => { if (error instanceof Error && error.message === "missing_beneficiary") { - throw new ConflictError(`Benficiary '${values.beneficiaryId}' does not exist`); + throw new BadRequestError(`Benficiary '${values.beneficiaryId}' does not exist`); } throw error; }); } export async function getLedgersByBeneficiary(beneficiaryId: string): Promise { - console.log( - await db.sql` - SELECT - *, _system_from as "createdAt" - FROM - payment.ledger - WHERE - "beneficiaryId" = ${beneficiaryId} - `, - ); return db.schema(LedgerSchema).many` SELECT *, _system_from as "createdAt" @@ -48,7 +50,7 @@ export async function getLedgersByBeneficiary(beneficiaryId: string): Promise { return db.schema(LedgerSchema).one` SELECT - *, _system_from as "createdAt" + *, _system_from as "created_at" FROM payment.ledger WHERE diff --git a/modules/payment/schemas/ledger.ts b/modules/payment/schemas/ledger.ts index 406652b..068537d 100644 --- a/modules/payment/schemas/ledger.ts +++ b/modules/payment/schemas/ledger.ts @@ -14,7 +14,7 @@ import { CurrencySchema } from "./currency.ts"; export const LedgerSchema = z.strictObject({ _id: z.uuid().describe("Primary identifier of the ledger"), beneficiaryId: z.uuid().describe("Identifier of the beneficiary this ledger belongs to"), - label: z.string().optional().describe("Human-readable identifier for the ledger"), + label: z.string().nullable().optional().describe("Human-readable identifier for the ledger"), currencies: z.array(CurrencySchema).describe("Currency this ledger trades in"), createdAt: z.coerce.date().describe("Timestamp the ledger was created"), }); diff --git a/platform/database/client.ts b/platform/database/client.ts index 14604a2..5a546b7 100644 --- a/platform/database/client.ts +++ b/platform/database/client.ts @@ -1,5 +1,4 @@ import { AsyncLocalStorage } from "node:async_hooks"; -import { serialize } from "node:v8"; import { takeAll, takeOne } from "@platform/parse"; import postgres, { type Options, type Sql, type TransactionSql } from "postgres"; @@ -9,7 +8,16 @@ import type { ZodType } from "zod"; const storage = new AsyncLocalStorage(); const transitReader = transit.reader("json"); -const transitWriter = transit.writer("json"); +const transitWriter = transit.writer("json", { + handlers: transit.map([ + Array, + transit.makeWriteHandler({ + tag: () => "array", + rep: (arr) => arr, + stringRep: () => null, + }), + ]), +}); /* |-------------------------------------------------------------------------------- @@ -66,6 +74,8 @@ export class Client { if (this.#db === undefined) { this.#db = postgres({ ...this.config, + transform: postgres.camel, + fetch_types: false, connection: { fallback_output_format: "transit", }, @@ -80,6 +90,15 @@ export class Client { return transitReader.read(value); }, }, + bool: { + to: 16, + } as unknown as postgres.PostgresType, + textArray: { + from: [1009], + parse: (value: unknown) => { + return parsePgArray(value); + }, + } as unknown as postgres.PostgresType, int64: { from: [20], parse: (value: string) => { @@ -90,6 +109,11 @@ export class Client { return res; }, } as unknown as postgres.PostgresType, + int: { + to: 20, + from: [23, 20], + parse: parseInt, + } as unknown as postgres.PostgresType, }, }); } @@ -167,11 +191,27 @@ export class Client { return this.sql.typed(value, 701); } + array(value: string[]) { + return this.sql.unsafe(`ARRAY[${value.map((v) => `'${v.replace(/'/g, "''")}'`).join(",")}]`); + } + transit(value: object) { return this.sql.typed(value, 16384); } } +function parsePgArray(value: unknown) { + if (typeof value !== "string") { + return value; + } + if (value.startsWith("{") && value.endsWith("}")) { + const content = value.slice(1, -1); + // Split by comma and strip quotes from each element + return content ? content.split(",").map((v) => v.trim().replace(/^"|"$/g, "")) : []; + } + return value; +} + /* |-------------------------------------------------------------------------------- | Types