feat(finance): BTC basket flows, client scoping, and jewelry-box store

- Finance API: baskets, holdings, rebalances, deposits, bridge withdrawals, vault checks.
- Schemas: btc-basket; api-client finance types; workspace lockfile update.
- Vitest config for finance service; expanded tests.

Made-with: Cursor
This commit is contained in:
defiQUG
2026-04-07 22:59:32 -07:00
parent 923b703d97
commit 3f7cc0f854
18 changed files with 1825 additions and 494 deletions

View File

@@ -21,6 +21,81 @@ export interface LedgerEntry {
reference?: string;
}
export interface BasketAllocation {
symbol: string;
targetWeightBps: number;
routeHint?: string;
}
export interface BasketMandate {
id: string;
clientId: string;
mandateName: string;
chain138VaultAddress: string;
baseAssetSymbol: string;
status: 'draft' | 'active' | 'rebalancing' | 'closed';
allocations: BasketAllocation[];
createdAt: string;
updatedAt: string;
}
export interface BtcDeposit {
id: string;
clientId: string;
basketId: string;
chain138VaultAddress: string;
depositAddress: string;
expectedAmountSats?: number;
confirmationsRequired: number;
currentConfirmations: number;
status:
| 'instruction_created'
| 'pending_confirmations'
| 'confirmed'
| 'minted'
| 'frozen';
observedTxId?: string;
freezeReason?: string;
createdAt: string;
updatedAt: string;
}
export interface Holding {
clientId: string;
basketId: string;
symbol: string;
allocationWeightBps: number;
bookValueSats: number;
status: 'pending_funding' | 'funded';
updatedAt: string;
}
export interface Rebalance {
id: string;
clientId: string;
basketId: string;
sourceSymbol: string;
targetSymbols: string[];
reason: string;
status: 'planned' | 'queued' | 'executed' | 'failed';
createdAt: string;
updatedAt: string;
}
export interface BridgeWithdrawal {
id: string;
clientId: string;
basketId: string;
sourceSymbol: string;
destinationSymbol: string;
destinationChainId: number;
destinationAddress: string;
amount: string;
status: 'pending' | 'approved' | 'submitted' | 'failed';
createdAt: string;
updatedAt: string;
}
export class FinanceClient {
protected client: AxiosInstance;
@@ -28,8 +103,8 @@ export class FinanceClient {
const apiBaseURL =
baseURL ||
(typeof window !== 'undefined'
? process.env.NEXT_PUBLIC_FINANCE_SERVICE_URL || 'http://localhost:4005'
: 'http://localhost:4005');
? process.env.NEXT_PUBLIC_FINANCE_SERVICE_URL || 'http://localhost:4003'
: 'http://localhost:4003');
this.client = axios.create({
baseURL: apiBaseURL,
@@ -38,7 +113,6 @@ export class FinanceClient {
},
});
// Set up request interceptor for authentication
this.client.interceptors.request.use(
(config) => {
const token = this.getAuthToken();
@@ -47,7 +121,7 @@ export class FinanceClient {
}
return config;
},
(error) => Promise.reject(error)
(error) => Promise.reject(error),
);
}
@@ -74,8 +148,8 @@ export class FinanceClient {
paymentMethod: string;
description?: string;
}): Promise<Payment> {
const response = await this.client.post<Payment>('/api/v1/payments', data);
return response.data;
const response = await this.client.post<{ payment: Payment }>('/api/v1/payments', data);
return response.data.payment;
}
async getPayment(paymentId: string): Promise<Payment> {
@@ -104,8 +178,71 @@ export class FinanceClient {
}): Promise<{ entries: LedgerEntry[]; total: number }> {
const response = await this.client.get<{ entries: LedgerEntry[]; total: number }>(
'/api/v1/ledger',
{ params: filters }
{ params: filters },
);
return response.data;
}
async createBasket(data: {
clientId: string;
mandateName: string;
chain138VaultAddress: string;
allocations: BasketAllocation[];
baseAssetSymbol?: string;
}): Promise<BasketMandate> {
const response = await this.client.post<{ basket: BasketMandate }>('/api/v1/baskets', data);
return response.data.basket;
}
async createBtcDeposit(data: {
clientId: string;
basketId?: string;
mandateName?: string;
chain138VaultAddress: string;
allocations?: BasketAllocation[];
expectedAmountSats?: number;
clientReference?: string;
}): Promise<{ deposit: BtcDeposit; basket: BasketMandate; createdBasket: boolean }> {
const response = await this.client.post<{
deposit: BtcDeposit;
basket: BasketMandate;
createdBasket: boolean;
}>('/api/v1/btc-deposits', data);
return response.data;
}
async getBtcDeposit(id: string): Promise<BtcDeposit> {
const response = await this.client.get<{ deposit: BtcDeposit }>(`/api/v1/btc-deposits/${id}`);
return response.data.deposit;
}
async getHoldings(filters?: { clientId?: string; basketId?: string }): Promise<Holding[]> {
const response = await this.client.get<{ holdings: Holding[] }>('/api/v1/holdings', {
params: filters,
});
return response.data.holdings;
}
async getRebalances(filters?: { clientId?: string; basketId?: string }): Promise<Rebalance[]> {
const response = await this.client.get<{ rebalances: Rebalance[] }>('/api/v1/rebalances', {
params: filters,
});
return response.data.rebalances;
}
async requestBridgeWithdrawal(data: {
clientId: string;
basketId: string;
sourceSymbol?: string;
destinationSymbol?: string;
destinationChainId: number;
destinationAddress: string;
amount: string;
}): Promise<BridgeWithdrawal> {
const response = await this.client.post<{ withdrawal: BridgeWithdrawal }>(
'/api/v1/withdrawals/bridge',
data,
);
return response.data.withdrawal;
}
}

View File

@@ -14,5 +14,14 @@ export type {
} from './identity';
export type { SubmitApplicationRequest, AdjudicateRequest } from './eresidency';
export type { DocumentUpload, DocumentMetadata } from './intake';
export type { Payment, LedgerEntry } from './finance';
export type {
Payment,
LedgerEntry,
BasketAllocation,
BasketMandate,
BtcDeposit,
Holding,
Rebalance,
BridgeWithdrawal,
} from './finance';
export type { DealRoom, Document } from './dataroom';

View File

@@ -6,11 +6,13 @@
import fetch from 'node-fetch';
import { createVerify, createPublicKey } from 'crypto';
import { decode as multibaseDecode } from 'multibase';
import base58 from 'base58-universal';
import { importJWK } from 'jose';
import forge from 'node-forge';
import { verify as ed25519Verify } from '@noble/ed25519';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { decode: base58Decode }: { decode: (value: string) => Uint8Array } = require('base58-universal');
export interface DIDDocument {
id: string;
'@context': string[];
@@ -89,7 +91,7 @@ export class DIDResolver {
if (multibaseKey.startsWith('z')) {
try {
const base58Encoded = multibaseKey.slice(1);
const decoded = base58.decode(base58Encoded);
const decoded = base58Decode(base58Encoded);
return Buffer.from(decoded);
} catch {
throw new Error('Failed to decode multibase key');
@@ -296,4 +298,3 @@ export class DIDResolver {
}
}
}

View File

@@ -122,6 +122,9 @@ export async function prepareCredentialImage(
if (imageData.startsWith('data:')) {
// Extract base64 data
const base64Data = imageData.split(',')[1];
if (!base64Data) {
throw new Error('Invalid image data URL: missing base64 payload');
}
imageBuffer = Buffer.from(base64Data, 'base64');
} else {
imageBuffer = Buffer.from(imageData);
@@ -180,4 +183,3 @@ export function getRecommendedImageSpecs(): {
maxSizeKB: 100, // Max 100KB recommended
};
}

View File

@@ -4,7 +4,6 @@
*/
import { query } from './client';
import { listDocuments, getDocumentById } from './schema';
export interface DocumentSearchResult {
documents: Array<{
@@ -102,8 +101,8 @@ export async function searchDocuments(
/**
* Get search suggestions
*/
export async function getSearchSuggestions(query: string, limit = 10): Promise<string[]> {
if (!query || query.length < 2) {
export async function getSearchSuggestions(searchTerm: string, limit = 10): Promise<string[]> {
if (!searchTerm || searchTerm.length < 2) {
return [];
}
@@ -113,9 +112,8 @@ export async function getSearchSuggestions(query: string, limit = 10): Promise<s
WHERE title ILIKE $1
ORDER BY title
LIMIT $2`,
[`%${query}%`, limit]
[`%${searchTerm}%`, limit]
);
return result.rows.map((row) => row.title);
}

View File

@@ -320,9 +320,11 @@ export function extractTemplateVariables(template_content: string): string[] {
const matches = template_content.matchAll(/\{\{(\w+(?:\.\w+)*)\}\}/g);
for (const match of matches) {
variables.add(match[1]);
const variableName = match[1];
if (variableName) {
variables.add(variableName);
}
}
return Array.from(variables).sort();
}

View File

@@ -0,0 +1,136 @@
import { z } from 'zod';
const EthAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/);
export const BasketAllocationSchema = z.object({
symbol: z.string().min(2).max(16),
targetWeightBps: z.number().int().positive().max(10_000),
routeHint: z.string().optional(),
});
export type BasketAllocation = z.infer<typeof BasketAllocationSchema>;
export const BasketStatusSchema = z.enum(['draft', 'active', 'rebalancing', 'closed']);
export const BasketMandateSchema = z.object({
id: z.string().uuid(),
clientId: z.string().min(1),
mandateName: z.string().min(1),
chain138VaultAddress: EthAddressSchema,
baseAssetSymbol: z.string().min(2).max(16).default('cBTC'),
status: BasketStatusSchema,
allocations: z.array(BasketAllocationSchema).min(1),
createdAt: z.date().or(z.string().datetime()),
updatedAt: z.date().or(z.string().datetime()),
});
export type BasketMandate = z.infer<typeof BasketMandateSchema>;
export const CreateBasketMandateSchema = BasketMandateSchema.omit({
id: true,
status: true,
createdAt: true,
updatedAt: true,
}).extend({
baseAssetSymbol: z.string().min(2).max(16).default('cBTC'),
});
export type CreateBasketMandate = z.infer<typeof CreateBasketMandateSchema>;
export const BtcDepositStatusSchema = z.enum([
'instruction_created',
'pending_confirmations',
'confirmed',
'minted',
'frozen',
]);
export const BtcDepositSchema = z.object({
id: z.string().uuid(),
clientId: z.string().min(1),
basketId: z.string().uuid(),
chain138VaultAddress: EthAddressSchema,
depositAddress: z.string().min(16),
expectedAmountSats: z.number().int().positive().optional(),
confirmationsRequired: z.number().int().positive(),
currentConfirmations: z.number().int().nonnegative(),
status: BtcDepositStatusSchema,
observedTxId: z.string().optional(),
freezeReason: z.string().optional(),
createdAt: z.date().or(z.string().datetime()),
updatedAt: z.date().or(z.string().datetime()),
});
export type BtcDeposit = z.infer<typeof BtcDepositSchema>;
export const CreateBtcDepositSchema = z.object({
clientId: z.string().min(1),
basketId: z.string().uuid().optional(),
mandateName: z.string().min(1).optional(),
chain138VaultAddress: EthAddressSchema,
allocations: z.array(BasketAllocationSchema).min(1).optional(),
expectedAmountSats: z.number().int().positive().optional(),
clientReference: z.string().min(1).optional(),
});
export type CreateBtcDeposit = z.infer<typeof CreateBtcDepositSchema>;
export const HoldingStatusSchema = z.enum(['pending_funding', 'funded']);
export const HoldingSchema = z.object({
clientId: z.string().min(1),
basketId: z.string().uuid(),
symbol: z.string().min(2).max(16),
allocationWeightBps: z.number().int().min(0).max(10_000),
bookValueSats: z.number().int().nonnegative(),
status: HoldingStatusSchema,
updatedAt: z.date().or(z.string().datetime()),
});
export type Holding = z.infer<typeof HoldingSchema>;
export const RebalanceStatusSchema = z.enum(['planned', 'queued', 'executed', 'failed']);
export const RebalanceSchema = z.object({
id: z.string().uuid(),
clientId: z.string().min(1),
basketId: z.string().uuid(),
sourceSymbol: z.string().min(2).max(16),
targetSymbols: z.array(z.string().min(2).max(16)).min(1),
reason: z.string().min(1),
status: RebalanceStatusSchema,
createdAt: z.date().or(z.string().datetime()),
updatedAt: z.date().or(z.string().datetime()),
});
export type Rebalance = z.infer<typeof RebalanceSchema>;
export const BridgeWithdrawalStatusSchema = z.enum(['pending', 'approved', 'submitted', 'failed']);
export const BridgeWithdrawalSchema = z.object({
id: z.string().uuid(),
clientId: z.string().min(1),
basketId: z.string().uuid(),
sourceSymbol: z.string().min(2).max(16),
destinationSymbol: z.string().min(2).max(16),
destinationChainId: z.number().int().positive(),
destinationAddress: EthAddressSchema,
amount: z.string().min(1),
status: BridgeWithdrawalStatusSchema,
createdAt: z.date().or(z.string().datetime()),
updatedAt: z.date().or(z.string().datetime()),
});
export type BridgeWithdrawal = z.infer<typeof BridgeWithdrawalSchema>;
export const CreateBridgeWithdrawalSchema = BridgeWithdrawalSchema.omit({
id: true,
status: true,
createdAt: true,
updatedAt: true,
}).extend({
sourceSymbol: z.string().min(2).max(16).optional(),
destinationSymbol: z.string().min(2).max(16).optional(),
});
export type CreateBridgeWithdrawal = z.infer<typeof CreateBridgeWithdrawalSchema>;

View File

@@ -9,4 +9,4 @@ export * from './vc';
export * from './payment';
export * from './ledger';
export * from './eresidency';
export * from './btc-basket';

View File

@@ -77,9 +77,9 @@ export function createSecurityTestData() {
export async function testAuthenticationBypass(
makeRequest: (headers?: Record<string, string>) => Promise<{ status: number }>
): Promise<boolean> {
const testCases = [
const testCases: Array<Record<string, string> | undefined> = [
// Missing token
{},
undefined,
// Invalid token
{ Authorization: 'Bearer invalid-token' },
// Expired token
@@ -226,4 +226,3 @@ export async function testCSRFProtection(
return true; // CSRF protection is working correctly
}

View File

@@ -5,7 +5,6 @@
import { FastifyInstance } from 'fastify';
import Fastify from 'fastify';
import { getEnv } from '@the-order/shared';
export interface ServerFactoryOptions {
port?: number;
@@ -41,4 +40,3 @@ export async function createTestServer(
export function closeTestServer(server: FastifyInstance): Promise<void> {
return server.close();
}

View File

@@ -62,7 +62,10 @@ export async function reviewWorkflow(
if (process.env.APPROVAL_SERVICE_URL) {
try {
const res = await fetch(`${process.env.APPROVAL_SERVICE_URL}/status/${input.documentId}/${input.reviewerId}`);
if (res.ok) approved = (await res.json()).approved ?? false;
if (res.ok) {
const approvalResponse = (await res.json()) as { approved?: boolean };
approved = approvalResponse.approved ?? false;
}
} catch { /* fall through */ }
}
if (!approved && getApprovalStatus) {
@@ -151,4 +154,3 @@ export async function checkApprovalStatus(
// Fallback: assume not approved if we can't check
return false;
}

522
pnpm-lock.yaml generated
View File

@@ -527,31 +527,6 @@ importers:
specifier: ^5.3.3
version: 5.9.3
packages/secrets:
dependencies:
'@aws-sdk/client-secrets-manager':
specifier: ^3.490.0
version: 3.927.0
'@azure/identity':
specifier: ^4.0.1
version: 4.13.0
'@azure/keyvault-secrets':
specifier: ^4.7.0
version: 4.10.0
zod:
specifier: ^3.22.4
version: 3.25.76
devDependencies:
'@types/node':
specifier: ^20.10.6
version: 20.19.24
typescript:
specifier: ^5.3.3
version: 5.9.3
vitest:
specifier: ^1.6.1
version: 1.6.1(@types/node@20.19.24)(@vitest/ui@1.6.1)
packages/shared:
dependencies:
'@fastify/cors':
@@ -796,6 +771,9 @@ importers:
'@fastify/swagger-ui':
specifier: ^2.0.0
version: 2.1.0
'@the-order/database':
specifier: workspace:*
version: link:../../packages/database
'@the-order/payment-gateway':
specifier: workspace:^
version: link:../../packages/payment-gateway
@@ -821,6 +799,9 @@ importers:
typescript:
specifier: ^5.3.3
version: 5.9.3
vitest:
specifier: ^1.6.1
version: 1.6.1(@types/node@20.19.24)(@vitest/ui@1.6.1)
services/identity:
dependencies:
@@ -899,6 +880,55 @@ importers:
specifier: ^5.3.3
version: 5.9.3
services/legal-documents:
dependencies:
'@fastify/swagger':
specifier: ^8.12.0
version: 8.15.0
'@fastify/swagger-ui':
specifier: ^1.9.3
version: 1.10.2
'@the-order/database':
specifier: workspace:*
version: link:../../packages/database
'@the-order/schemas':
specifier: workspace:*
version: link:../../packages/schemas
'@the-order/shared':
specifier: workspace:*
version: link:../../packages/shared
'@the-order/storage':
specifier: workspace:*
version: link:../../packages/storage
docx:
specifier: ^8.5.0
version: 8.6.0
fastify:
specifier: ^4.24.3
version: 4.29.1
pdf-lib:
specifier: ^1.17.1
version: 1.17.1
pdfkit:
specifier: ^0.15.0
version: 0.15.2
zod:
specifier: ^3.22.4
version: 3.25.76
devDependencies:
'@types/node':
specifier: ^20.10.0
version: 20.19.24
tsx:
specifier: ^4.7.0
version: 4.20.6
typescript:
specifier: ^5.3.3
version: 5.9.3
vitest:
specifier: ^1.1.0
version: 1.6.1(@types/node@20.19.24)(@vitest/ui@1.6.1)
packages:
/@alloc/quick-lru@5.2.0:
@@ -1081,54 +1111,6 @@ packages:
- aws-crt
dev: false
/@aws-sdk/client-secrets-manager@3.927.0:
resolution: {integrity: sha512-6hdJ4OHyM4NvOS/uzt06Ko1NLhI04qwIq1nwH1kmo2W6VOc9ozLJnzIRlUCVw6s/F8dQtvfsCzFf0rJshGyT6Q==}
engines: {node: '>=18.0.0'}
dependencies:
'@aws-crypto/sha256-browser': 5.2.0
'@aws-crypto/sha256-js': 5.2.0
'@aws-sdk/core': 3.927.0
'@aws-sdk/credential-provider-node': 3.927.0
'@aws-sdk/middleware-host-header': 3.922.0
'@aws-sdk/middleware-logger': 3.922.0
'@aws-sdk/middleware-recursion-detection': 3.922.0
'@aws-sdk/middleware-user-agent': 3.927.0
'@aws-sdk/region-config-resolver': 3.925.0
'@aws-sdk/types': 3.922.0
'@aws-sdk/util-endpoints': 3.922.0
'@aws-sdk/util-user-agent-browser': 3.922.0
'@aws-sdk/util-user-agent-node': 3.927.0
'@smithy/config-resolver': 4.4.2
'@smithy/core': 3.17.2
'@smithy/fetch-http-handler': 5.3.5
'@smithy/hash-node': 4.2.4
'@smithy/invalid-dependency': 4.2.4
'@smithy/middleware-content-length': 4.2.4
'@smithy/middleware-endpoint': 4.3.6
'@smithy/middleware-retry': 4.4.6
'@smithy/middleware-serde': 4.2.4
'@smithy/middleware-stack': 4.2.4
'@smithy/node-config-provider': 4.3.4
'@smithy/node-http-handler': 4.4.4
'@smithy/protocol-http': 5.3.4
'@smithy/smithy-client': 4.9.2
'@smithy/types': 4.8.1
'@smithy/url-parser': 4.2.4
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-body-length-node': 4.2.1
'@smithy/util-defaults-mode-browser': 4.3.5
'@smithy/util-defaults-mode-node': 4.2.8
'@smithy/util-endpoints': 3.2.4
'@smithy/util-middleware': 4.2.4
'@smithy/util-retry': 4.2.4
'@smithy/util-utf8': 4.2.0
'@smithy/uuid': 1.1.0
tslib: 2.8.1
transitivePeerDependencies:
- aws-crt
dev: false
/@aws-sdk/client-sfn@3.928.0:
resolution: {integrity: sha512-GiFBGqoh+69XxN3yvykxNibpnjDOnBxzzyfxWl4k8Q73qdKWnr/x+4SiCRqZXKbbUiszbPGexYJG9L5w9emfAA==}
engines: {node: '>=18.0.0'}
@@ -1921,20 +1903,6 @@ packages:
engines: {node: '>=18.0.0'}
dev: false
/@azure-rest/core-client@2.5.1:
resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==}
engines: {node: '>=20.0.0'}
dependencies:
'@azure/abort-controller': 2.1.2
'@azure/core-auth': 1.10.1
'@azure/core-rest-pipeline': 1.22.2
'@azure/core-tracing': 1.3.1
'@typespec/ts-http-runtime': 0.3.2
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
dev: false
/@azure/abort-controller@2.1.2:
resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==}
engines: {node: '>=18.0.0'}
@@ -1968,36 +1936,6 @@ packages:
- supports-color
dev: false
/@azure/core-http-compat@2.3.1:
resolution: {integrity: sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==}
engines: {node: '>=20.0.0'}
dependencies:
'@azure/abort-controller': 2.1.2
'@azure/core-client': 1.10.1
'@azure/core-rest-pipeline': 1.22.2
transitivePeerDependencies:
- supports-color
dev: false
/@azure/core-lro@2.7.2:
resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==}
engines: {node: '>=18.0.0'}
dependencies:
'@azure/abort-controller': 2.1.2
'@azure/core-util': 1.13.1
'@azure/logger': 1.3.0
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
dev: false
/@azure/core-paging@1.6.2:
resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==}
engines: {node: '>=18.0.0'}
dependencies:
tslib: 2.8.1
dev: false
/@azure/core-rest-pipeline@1.22.2:
resolution: {integrity: sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==}
engines: {node: '>=20.0.0'}
@@ -2050,42 +1988,6 @@ packages:
- supports-color
dev: false
/@azure/keyvault-common@2.0.0:
resolution: {integrity: sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==}
engines: {node: '>=18.0.0'}
dependencies:
'@azure/abort-controller': 2.1.2
'@azure/core-auth': 1.10.1
'@azure/core-client': 1.10.1
'@azure/core-rest-pipeline': 1.22.2
'@azure/core-tracing': 1.3.1
'@azure/core-util': 1.13.1
'@azure/logger': 1.3.0
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
dev: false
/@azure/keyvault-secrets@4.10.0:
resolution: {integrity: sha512-WvXc3h2hqHL1pMzUU7ANE2RBKoxjK3JQc0YNn6GUFvOWQtf2ZR+sH4/5cZu8zAg62v9qLCduBN7065nHKl+AOA==}
engines: {node: '>=18.0.0'}
dependencies:
'@azure-rest/core-client': 2.5.1
'@azure/abort-controller': 2.1.2
'@azure/core-auth': 1.10.1
'@azure/core-http-compat': 2.3.1
'@azure/core-lro': 2.7.2
'@azure/core-paging': 1.6.2
'@azure/core-rest-pipeline': 1.22.2
'@azure/core-tracing': 1.3.1
'@azure/core-util': 1.13.1
'@azure/keyvault-common': 2.0.0
'@azure/logger': 1.3.0
tslib: 2.8.1
transitivePeerDependencies:
- supports-color
dev: false
/@azure/logger@1.3.0:
resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==}
engines: {node: '>=20.0.0'}
@@ -2757,6 +2659,16 @@ packages:
p-limit: 3.1.0
dev: false
/@fastify/swagger-ui@1.10.2:
resolution: {integrity: sha512-f2mRqtblm6eRAFQ3e8zSngxVNEtiYY7rISKQVjPA++ZsWc5WYlPVTb6Bx0G/zy0BIoucNqDr/Q2Vb/kTYkOq1A==}
dependencies:
'@fastify/static': 6.12.0
fastify-plugin: 4.5.1
openapi-types: 12.1.3
rfdc: 1.4.1
yaml: 2.8.1
dev: false
/@fastify/swagger-ui@2.1.0:
resolution: {integrity: sha512-mu0C28kMEQDa3miE8f3LmI/OQSmqaKS3dYhZVFO5y4JdgBIPbzZj6COCoRU/P/9nu7UogzzcCJtg89wwLwKtWg==}
dependencies:
@@ -4665,6 +4577,18 @@ packages:
'@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
dev: false
/@pdf-lib/standard-fonts@1.0.0:
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
dependencies:
pako: 1.0.11
dev: false
/@pdf-lib/upng@1.0.1:
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
dependencies:
pako: 1.0.11
dev: false
/@pinojs/redact@0.4.0:
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
dev: false
@@ -5440,6 +5364,12 @@ packages:
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
dev: false
/@swc/helpers@0.3.17:
resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==}
dependencies:
tslib: 2.8.1
dev: false
/@swc/helpers@0.5.5:
resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
dependencies:
@@ -6384,7 +6314,6 @@ packages:
dependencies:
call-bound: 1.0.4
is-array-buffer: 3.0.5
dev: true
/array-includes@3.1.9:
resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
@@ -6522,7 +6451,6 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
possible-typed-array-names: 1.1.0
dev: true
/avvio@8.4.0:
resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==}
@@ -6559,6 +6487,11 @@ packages:
engines: {node: '>=14'}
dev: false
/base64-js@0.0.8:
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
engines: {node: '>= 0.4'}
dev: false
/base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -6619,6 +6552,12 @@ packages:
dependencies:
fill-range: 7.1.1
/brotli@1.3.3:
resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==}
dependencies:
base64-js: 1.5.1
dev: false
/browserslist@4.28.0:
resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -6701,7 +6640,6 @@ packages:
es-define-property: 1.0.1
get-intrinsic: 1.3.0
set-function-length: 1.2.2
dev: true
/call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
@@ -6891,6 +6829,11 @@ packages:
engines: {node: '>=0.8'}
dev: true
/clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
engines: {node: '>=0.8'}
dev: false
/clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -6980,6 +6923,10 @@ packages:
requiresBuild: true
dev: true
/core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: false
/create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
@@ -6999,6 +6946,10 @@ packages:
shebang-command: 2.0.0
which: 2.0.2
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -7160,6 +7111,30 @@ packages:
dependencies:
type-detect: 4.1.0
/deep-equal@2.2.3:
resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
engines: {node: '>= 0.4'}
dependencies:
array-buffer-byte-length: 1.0.2
call-bind: 1.0.8
es-get-iterator: 1.1.3
get-intrinsic: 1.3.0
is-arguments: 1.2.0
is-array-buffer: 3.0.5
is-date-object: 1.1.0
is-regex: 1.2.1
is-shared-array-buffer: 1.0.4
isarray: 2.0.5
object-is: 1.1.6
object-keys: 1.1.1
object.assign: 4.1.7
regexp.prototype.flags: 1.5.4
side-channel: 1.1.0
which-boxed-primitive: 1.1.1
which-collection: 1.0.2
which-typed-array: 1.1.19
dev: false
/deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
@@ -7195,7 +7170,6 @@ packages:
es-define-property: 1.0.1
es-errors: 1.3.0
gopd: 1.2.0
dev: true
/define-lazy-prop@3.0.0:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
@@ -7209,7 +7183,6 @@ packages:
define-data-property: 1.1.4
has-property-descriptors: 1.0.2
object-keys: 1.1.1
dev: true
/degenerator@5.0.1:
resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
@@ -7254,6 +7227,10 @@ packages:
requiresBuild: true
dev: false
/dfa@1.2.0:
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
dev: false
/didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dev: true
@@ -7292,6 +7269,18 @@ packages:
esutils: 2.0.3
dev: true
/docx@8.6.0:
resolution: {integrity: sha512-JEzPozEsuGIyUkEqdGlCv/b1avYeXjR4PjwiLdPwRKdsI9spBCq4WP0QcGYfIANpgYdJSphw4IT8M/a9dpnpvQ==}
engines: {node: '>=10'}
deprecated: A API breaking change was introduced here, which was incompatible with semantic versioning
dependencies:
'@types/node': 20.19.24
jszip: 3.10.1
nanoid: 5.1.7
xml: 1.0.1
xml-js: 1.6.11
dev: false
/dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies:
@@ -7417,6 +7406,20 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
/es-get-iterator@1.1.3:
resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
dependencies:
call-bind: 1.0.8
get-intrinsic: 1.3.0
has-symbols: 1.1.0
is-arguments: 1.2.0
is-map: 2.0.3
is-set: 2.0.3
is-string: 1.1.1
isarray: 2.0.5
stop-iteration-iterator: 1.1.0
dev: false
/es-iterator-helpers@1.2.1:
resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==}
engines: {node: '>= 0.4'}
@@ -8212,12 +8215,25 @@ packages:
optional: true
dev: false
/fontkit@1.9.0:
resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==}
dependencies:
'@swc/helpers': 0.3.17
brotli: 1.3.3
clone: 2.1.2
deep-equal: 2.2.3
dfa: 1.2.0
restructure: 2.0.1
tiny-inflate: 1.0.3
unicode-properties: 1.4.1
unicode-trie: 2.0.0
dev: false
/for-each@0.3.5:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
dependencies:
is-callable: 1.2.7
dev: true
/foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
@@ -8282,7 +8298,6 @@ packages:
/functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: true
/gaxios@6.7.1:
resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
@@ -8513,7 +8528,6 @@ packages:
/has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
dev: true
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
@@ -8529,7 +8543,6 @@ packages:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
dependencies:
es-define-property: 1.0.1
dev: true
/has-proto@1.2.0:
resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
@@ -8644,6 +8657,10 @@ packages:
engines: {node: '>= 4'}
dev: true
/immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
dev: false
/import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -8743,7 +8760,6 @@ packages:
es-errors: 1.3.0
hasown: 2.0.2
side-channel: 1.1.0
dev: true
/internmap@2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
@@ -8777,6 +8793,14 @@ packages:
engines: {node: '>= 0.10'}
dev: false
/is-arguments@1.2.0:
resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
engines: {node: '>= 0.4'}
dependencies:
call-bound: 1.0.4
has-tostringtag: 1.0.2
dev: false
/is-array-buffer@3.0.5:
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
engines: {node: '>= 0.4'}
@@ -8784,7 +8808,6 @@ packages:
call-bind: 1.0.8
call-bound: 1.0.4
get-intrinsic: 1.3.0
dev: true
/is-async-function@2.1.1:
resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
@@ -8802,7 +8825,6 @@ packages:
engines: {node: '>= 0.4'}
dependencies:
has-bigints: 1.1.0
dev: true
/is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
@@ -8817,7 +8839,6 @@ packages:
dependencies:
call-bound: 1.0.4
has-tostringtag: 1.0.2
dev: true
/is-bun-module@2.0.0:
resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==}
@@ -8828,7 +8849,6 @@ packages:
/is-callable@1.2.7:
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
engines: {node: '>= 0.4'}
dev: true
/is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
@@ -8851,7 +8871,6 @@ packages:
dependencies:
call-bound: 1.0.4
has-tostringtag: 1.0.2
dev: true
/is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
@@ -8924,7 +8943,6 @@ packages:
/is-map@2.0.3:
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
engines: {node: '>= 0.4'}
dev: true
/is-negative-zero@2.0.3:
resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
@@ -8937,7 +8955,6 @@ packages:
dependencies:
call-bound: 1.0.4
has-tostringtag: 1.0.2
dev: true
/is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
@@ -8961,19 +8978,16 @@ packages:
gopd: 1.2.0
has-tostringtag: 1.0.2
hasown: 2.0.2
dev: true
/is-set@2.0.3:
resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
engines: {node: '>= 0.4'}
dev: true
/is-shared-array-buffer@1.0.4:
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
engines: {node: '>= 0.4'}
dependencies:
call-bound: 1.0.4
dev: true
/is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
@@ -8989,7 +9003,6 @@ packages:
dependencies:
call-bound: 1.0.4
has-tostringtag: 1.0.2
dev: true
/is-symbol@1.1.1:
resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
@@ -8998,7 +9011,6 @@ packages:
call-bound: 1.0.4
has-symbols: 1.1.0
safe-regex-test: 1.1.0
dev: true
/is-typed-array@1.1.15:
resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
@@ -9025,7 +9037,6 @@ packages:
/is-weakmap@2.0.2:
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
engines: {node: '>= 0.4'}
dev: true
/is-weakref@1.1.1:
resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
@@ -9040,7 +9051,6 @@ packages:
dependencies:
call-bound: 1.0.4
get-intrinsic: 1.3.0
dev: true
/is-wsl@3.1.0:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
@@ -9049,9 +9059,12 @@ packages:
is-inside-container: 1.0.0
dev: false
/isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: false
/isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
dev: true
/isbinaryfile@4.0.10:
resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==}
@@ -9096,6 +9109,11 @@ packages:
engines: {node: '>=10'}
dev: false
/jpeg-exif@1.1.4:
resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -9189,6 +9207,15 @@ packages:
object.values: 1.2.1
dev: true
/jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
dev: false
/jwa@1.4.2:
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
dependencies:
@@ -9229,6 +9256,12 @@ packages:
type-check: 0.4.0
dev: true
/lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
dependencies:
immediate: 3.0.6
dev: false
/light-my-request@5.14.0:
resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==}
dependencies:
@@ -9242,6 +9275,13 @@ packages:
engines: {node: '>=14'}
dev: true
/linebreak@1.1.0:
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
dependencies:
base64-js: 0.0.8
unicode-trie: 2.0.0
dev: false
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
@@ -9590,6 +9630,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/nanoid@5.1.7:
resolution: {integrity: sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ==}
engines: {node: ^18 || >=20}
hasBin: true
dev: false
/napi-postinstall@0.3.4:
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@@ -9763,10 +9809,17 @@ packages:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
/object-is@1.1.6:
resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.8
define-properties: 1.2.1
dev: false
/object-keys@1.1.1:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'}
dev: true
/object.assign@4.1.7:
resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
@@ -9778,7 +9831,6 @@ packages:
es-object-atoms: 1.1.1
has-symbols: 1.1.0
object-keys: 1.1.1
dev: true
/object.entries@1.1.9:
resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==}
@@ -9977,6 +10029,14 @@ packages:
netmask: 2.0.2
dev: true
/pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
dev: false
/pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
dev: false
/param-case@2.1.1:
resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==}
dependencies:
@@ -10046,6 +10106,25 @@ packages:
/pathval@1.1.1:
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
/pdf-lib@1.17.1:
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
dependencies:
'@pdf-lib/standard-fonts': 1.0.0
'@pdf-lib/upng': 1.0.1
pako: 1.0.11
tslib: 1.14.1
dev: false
/pdfkit@0.15.2:
resolution: {integrity: sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==}
dependencies:
crypto-js: 4.2.0
fontkit: 1.9.0
jpeg-exif: 1.1.4
linebreak: 1.1.0
png-js: 1.0.0
dev: false
/pg-cloudflare@1.2.7:
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
requiresBuild: true
@@ -10215,10 +10294,13 @@ packages:
mlly: 1.8.0
pathe: 2.0.3
/png-js@1.0.0:
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
dev: false
/possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
dev: true
/postcss-import@15.1.0(postcss@8.5.6):
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
@@ -10341,6 +10423,10 @@ packages:
ansi-styles: 5.2.0
react-is: 18.3.1
/process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: false
/process-warning@3.0.0:
resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
dev: false
@@ -10532,6 +10618,18 @@ packages:
pify: 2.3.0
dev: true
/readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
dev: false
/readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -10645,7 +10743,6 @@ packages:
get-proto: 1.0.1
gopd: 1.2.0
set-function-name: 2.0.2
dev: true
/registry-auth-token@3.3.2:
resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==}
@@ -10725,6 +10822,10 @@ packages:
signal-exit: 4.1.0
dev: true
/restructure@2.0.1:
resolution: {integrity: sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==}
dev: false
/ret@0.4.3:
resolution: {integrity: sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==}
engines: {node: '>=10'}
@@ -10815,6 +10916,10 @@ packages:
isarray: 2.0.5
dev: true
/safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
dev: false
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
@@ -10833,7 +10938,6 @@ packages:
call-bound: 1.0.4
es-errors: 1.3.0
is-regex: 1.2.1
dev: true
/safe-regex2@3.1.0:
resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==}
@@ -10855,6 +10959,11 @@ packages:
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
/sax@1.6.0:
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
engines: {node: '>=11.0.0'}
dev: false
/scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
dependencies:
@@ -10896,7 +11005,6 @@ packages:
get-intrinsic: 1.3.0
gopd: 1.2.0
has-property-descriptors: 1.0.2
dev: true
/set-function-name@2.0.2:
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
@@ -10906,7 +11014,6 @@ packages:
es-errors: 1.3.0
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
dev: true
/set-proto@1.0.0:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
@@ -10917,6 +11024,10 @@ packages:
es-object-atoms: 1.1.1
dev: true
/setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
dev: false
/setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
dev: false
@@ -11119,7 +11230,6 @@ packages:
dependencies:
es-errors: 1.3.0
internal-slot: 1.1.0
dev: true
/streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
@@ -11232,6 +11342,12 @@ packages:
es-object-atoms: 1.1.1
dev: true
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
safe-buffer: 5.1.2
dev: false
/string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies:
@@ -11451,6 +11567,10 @@ packages:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: true
/tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
dev: false
/tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
dev: false
@@ -11578,7 +11698,6 @@ packages:
/tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true
/tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -11767,6 +11886,20 @@ packages:
/undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
/unicode-properties@1.4.1:
resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==}
dependencies:
base64-js: 1.5.1
unicode-trie: 2.0.0
dev: false
/unicode-trie@2.0.0:
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
dependencies:
pako: 0.2.9
tiny-inflate: 1.0.3
dev: false
/universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -11847,7 +11980,6 @@ packages:
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
@@ -12038,7 +12170,6 @@ packages:
is-number-object: 1.1.1
is-string: 1.1.1
is-symbol: 1.1.1
dev: true
/which-builtin-type@1.2.1:
resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
@@ -12067,7 +12198,6 @@ packages:
is-set: 2.0.3
is-weakmap: 2.0.2
is-weakset: 2.0.4
dev: true
/which-typed-array@1.1.19:
resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
@@ -12080,7 +12210,6 @@ packages:
get-proto: 1.0.1
gopd: 1.2.0
has-tostringtag: 1.0.2
dev: true
/which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
@@ -12151,6 +12280,17 @@ packages:
is-wsl: 3.1.0
dev: false
/xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
dependencies:
sax: 1.6.0
dev: false
/xml@1.0.1:
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}

View File

@@ -9,11 +9,14 @@
"build": "tsc",
"start": "node dist/index.js",
"lint": "eslint src --ext .ts",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@fastify/swagger": "^8.13.0",
"@fastify/swagger-ui": "^2.0.0",
"@the-order/database": "workspace:*",
"@the-order/payment-gateway": "workspace:^",
"@the-order/schemas": "workspace:*",
"@the-order/shared": "workspace:*",
@@ -23,6 +26,7 @@
"@types/node": "^20.10.6",
"eslint": "^9.17.0",
"tsx": "^4.7.0",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^1.6.1"
}
}

View File

@@ -1,62 +1,324 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import { createApiHelpers } from '@the-order/test-utils';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
import { createHmac } from 'crypto';
import type { FastifyInstance } from 'fastify';
describe('Finance Service', () => {
let app: FastifyInstance;
let api: ReturnType<typeof createApiHelpers>;
const originalEnv = { ...process.env };
beforeEach(async () => {
app = Fastify({
logger: false,
});
function setTestEnv(): void {
process.env.NODE_ENV = 'development';
process.env.PORT = '4003';
process.env.DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:5432/the_order_test';
process.env.STORAGE_BUCKET = 'test-bucket';
process.env.KMS_KEY_ID = 'test-kms-key';
process.env.JWT_SECRET = 'test-jwt-secret-which-is-long-enough-1234567890';
}
app.get('/health', async () => {
return { status: 'ok', service: 'finance' };
});
function base64Url(value: string): string {
return Buffer.from(value).toString('base64url');
}
await app.ready();
api = createApiHelpers(app);
});
function createJwt(payload: Record<string, unknown>): string {
const header = base64Url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
const body = base64Url(JSON.stringify(payload));
const signature = createHmac('sha256', process.env.JWT_SECRET as string)
.update(`${header}.${body}`)
.digest('base64url');
return `${header}.${body}.${signature}`;
}
afterEach(async () => {
if (app) {
await app.close();
}
});
let createServer: (typeof import('./index'))['createServer'];
describe('GET /health', () => {
it('should return health status', async () => {
const response = await api.get('/health');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('status');
expect(response.body).toHaveProperty('service', 'finance');
});
});
describe('POST /ledger/entry', () => {
it('should require authentication', async () => {
const response = await api.post('/ledger/entry', {
accountId: 'test-account',
type: 'debit',
amount: 100,
currency: 'USD',
});
expect([401, 500]).toContain(response.status);
});
});
describe('POST /payments', () => {
it('should require authentication', async () => {
const response = await api.post('/payments', {
amount: 100,
currency: 'USD',
paymentMethod: 'credit_card',
});
expect([401, 500]).toContain(response.status);
});
});
beforeAll(async () => {
setTestEnv();
({ createServer } = await import('./index'));
});
describe('Finance Service', () => {
let server: FastifyInstance;
afterEach(async () => {
if (server) {
await server.close();
}
process.env = { ...originalEnv, ...process.env };
});
it('returns service health', async () => {
server = await createServer({ disableSwagger: true });
await server.ready();
const response = await server.inject({
method: 'GET',
url: '/health',
});
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({
service: 'finance',
});
});
it('creates a basket-backed BTC deposit and exposes holdings plus rebalance plan', async () => {
server = await createServer({ disableSwagger: true });
await server.ready();
const token = createJwt({
id: 'client-1',
roles: ['client', 'finance'],
});
const depositResponse = await server.inject({
method: 'POST',
url: '/api/v1/btc-deposits',
headers: {
authorization: `Bearer ${token}`,
},
payload: {
clientId: 'client-1',
mandateName: 'Solace BTC jewelry box',
chain138VaultAddress: '0x1111111111111111111111111111111111111111',
allocations: [
{ symbol: 'cBTC', targetWeightBps: 4000 },
{ symbol: 'cUSDT', targetWeightBps: 3500 },
{ symbol: 'cXAUC', targetWeightBps: 2500 },
],
expectedAmountSats: 250000000,
},
});
expect(depositResponse.statusCode).toBe(201);
const depositPayload = depositResponse.json() as {
deposit: { id: string; basketId: string; status: string; confirmationsRequired: number; depositAddress: string };
basket: { id: string };
createdBasket: boolean;
};
expect(depositPayload.createdBasket).toBe(true);
expect(depositPayload.deposit.status).toBe('instruction_created');
expect(depositPayload.deposit.confirmationsRequired).toBe(6);
expect(depositPayload.deposit.depositAddress.startsWith('bc1q')).toBe(true);
expect(depositPayload.deposit.basketId).toBe(depositPayload.basket.id);
const holdingsResponse = await server.inject({
method: 'GET',
url: `/api/v1/holdings?clientId=client-1&basketId=${depositPayload.basket.id}`,
headers: {
authorization: `Bearer ${token}`,
},
});
expect(holdingsResponse.statusCode).toBe(200);
const holdingsPayload = holdingsResponse.json() as {
holdings: Array<{ symbol: string; allocationWeightBps: number; status: string }>;
};
expect(holdingsPayload.holdings).toHaveLength(3);
expect(holdingsPayload.holdings[1]).toMatchObject({
symbol: 'cUSDT',
allocationWeightBps: 3500,
status: 'pending_funding',
});
const rebalancesResponse = await server.inject({
method: 'GET',
url: `/api/v1/rebalances?basketId=${depositPayload.basket.id}`,
headers: {
authorization: `Bearer ${token}`,
},
});
expect(rebalancesResponse.statusCode).toBe(200);
const rebalancesPayload = rebalancesResponse.json() as {
rebalances: Array<{ sourceSymbol: string; targetSymbols: string[]; status: string }>;
};
expect(rebalancesPayload.rebalances).toHaveLength(1);
expect(rebalancesPayload.rebalances[0]).toMatchObject({
sourceSymbol: 'cBTC',
status: 'planned',
});
expect(rebalancesPayload.rebalances[0].targetSymbols).toEqual(['cUSDT', 'cXAUC']);
});
it('defaults bridge withdrawals to cBTC -> cWBTC', async () => {
server = await createServer({ disableSwagger: true });
await server.ready();
const token = createJwt({
id: 'client-2',
roles: ['client'],
});
const basketResponse = await server.inject({
method: 'POST',
url: '/api/v1/baskets',
headers: {
authorization: `Bearer ${token}`,
},
payload: {
clientId: 'client-2',
mandateName: 'Client 2 basket',
chain138VaultAddress: '0x2222222222222222222222222222222222222222',
allocations: [{ symbol: 'cBTC', targetWeightBps: 10000 }],
},
});
const basketId = (basketResponse.json() as { basket: { id: string } }).basket.id;
const withdrawalResponse = await server.inject({
method: 'POST',
url: '/api/v1/withdrawals/bridge',
headers: {
authorization: `Bearer ${token}`,
},
payload: {
clientId: 'client-2',
basketId,
destinationChainId: 1,
destinationAddress: '0x3333333333333333333333333333333333333333',
amount: '125000000',
},
});
expect(withdrawalResponse.statusCode).toBe(201);
expect(withdrawalResponse.json()).toMatchObject({
withdrawal: {
sourceSymbol: 'cBTC',
destinationSymbol: 'cWBTC',
destinationChainId: 1,
},
});
});
it('rejects cross-client access to another client basket', async () => {
server = await createServer({ disableSwagger: true });
await server.ready();
const ownerToken = createJwt({
id: 'client-owner',
roles: ['client'],
});
const otherClientToken = createJwt({
id: 'client-other',
roles: ['client'],
});
const basketResponse = await server.inject({
method: 'POST',
url: '/api/v1/baskets',
headers: {
authorization: `Bearer ${ownerToken}`,
},
payload: {
clientId: 'client-owner',
mandateName: 'Owner basket',
chain138VaultAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
allocations: [{ symbol: 'cBTC', targetWeightBps: 10000 }],
},
});
const basketId = (basketResponse.json() as { basket: { id: string } }).basket.id;
const holdingsResponse = await server.inject({
method: 'GET',
url: `/api/v1/holdings?basketId=${basketId}`,
headers: {
authorization: `Bearer ${otherClientToken}`,
},
});
expect(holdingsResponse.statusCode).toBe(403);
const withdrawalResponse = await server.inject({
method: 'POST',
url: '/api/v1/withdrawals/bridge',
headers: {
authorization: `Bearer ${otherClientToken}`,
},
payload: {
clientId: 'client-other',
basketId,
destinationChainId: 1,
destinationAddress: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
amount: '1000',
},
});
expect(withdrawalResponse.statusCode).toBe(403);
});
it('rejects unknown basket ids and mismatched vault addresses on deposit creation', async () => {
server = await createServer({ disableSwagger: true });
await server.ready();
const token = createJwt({
id: 'client-3',
roles: ['client'],
});
const unknownBasketResponse = await server.inject({
method: 'POST',
url: '/api/v1/btc-deposits',
headers: {
authorization: `Bearer ${token}`,
},
payload: {
clientId: 'client-3',
basketId: '11111111-1111-4111-8111-111111111111',
chain138VaultAddress: '0xcccccccccccccccccccccccccccccccccccccccc',
},
});
expect(unknownBasketResponse.statusCode).toBe(404);
const basketResponse = await server.inject({
method: 'POST',
url: '/api/v1/baskets',
headers: {
authorization: `Bearer ${token}`,
},
payload: {
clientId: 'client-3',
mandateName: 'Vault match basket',
chain138VaultAddress: '0xdddddddddddddddddddddddddddddddddddddddd',
allocations: [{ symbol: 'cBTC', targetWeightBps: 10000 }],
},
});
const basketId = (basketResponse.json() as { basket: { id: string } }).basket.id;
const mismatchedVaultResponse = await server.inject({
method: 'POST',
url: '/api/v1/btc-deposits',
headers: {
authorization: `Bearer ${token}`,
},
payload: {
clientId: 'client-3',
basketId,
chain138VaultAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
},
});
expect(mismatchedVaultResponse.statusCode).toBe(400);
});
it('validates payment payloads on the versioned route', async () => {
server = await createServer({ disableSwagger: true });
await server.ready();
const token = createJwt({
id: 'finance-user',
roles: ['finance'],
});
const response = await server.inject({
method: 'POST',
url: '/api/v1/payments',
headers: {
authorization: `Bearer ${token}`,
},
payload: {},
});
expect(response.statusCode).toBe(400);
});
});

View File

@@ -1,191 +1,236 @@
/**
* Finance Service
* Handles payments, ledgers, rate models, and invoicing
* Handles payments, ledgers, and BTC jewelry-box workflows.
*/
import Fastify, { type FastifyRequest, type FastifyReply } from 'fastify';
import { fileURLToPath } from 'url';
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify';
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUI from '@fastify/swagger-ui';
import {
AppError,
errorHandler,
createLogger,
registerSecurityPlugins,
addCorrelationId,
addRequestLogging,
getEnv,
createBodySchema,
authenticateJWT,
requireRole,
} from '@the-order/shared';
import { CreateLedgerEntrySchema, CreatePaymentSchema } from '@the-order/schemas';
import { healthCheck as dbHealthCheck, getPool, createLedgerEntry, createPayment, updatePaymentStatus } from '@the-order/database';
import {
type BasketMandate,
type CreateBasketMandate,
type CreateBtcDeposit,
type CreateBridgeWithdrawal,
} from '@the-order/schemas';
import {
healthCheck as dbHealthCheck,
getPool,
createLedgerEntry,
createPayment,
updatePaymentStatus,
} from '@the-order/database';
import { StripePaymentGateway } from '@the-order/payment-gateway';
import {
JewelryBoxStore,
createDefaultBridgeWithdrawal,
} from './jewelry-box-store';
const logger = createLogger('finance-service');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const server: any = Fastify({
logger: logger as any,
requestIdLogLabel: 'requestId',
disableRequestLogging: false,
});
// Initialize database pool
const env = getEnv();
if (env.DATABASE_URL) {
getPool({ connectionString: env.DATABASE_URL });
const PRIVILEGED_FINANCE_ROLES = new Set(['admin', 'accountant', 'finance']);
export type CreateServerOptions = {
jewelryBoxStore?: JewelryBoxStore;
disableSwagger?: boolean;
};
function isPrivilegedFinanceUser(request: FastifyRequest): boolean {
return (request.user?.roles ?? []).some((role) => PRIVILEGED_FINANCE_ROLES.has(role));
}
// Initialize payment gateway
let paymentGateway: StripePaymentGateway | null = null;
try {
if (env.PAYMENT_GATEWAY_API_KEY) {
paymentGateway = new StripePaymentGateway();
function assertClientAccess(request: FastifyRequest, clientId: string): void {
if (!request.user) {
throw new AppError(401, 'UNAUTHORIZED', 'Authentication required');
}
} catch (error) {
logger.warn({ err: error }, 'Payment gateway not configured');
if (request.user.id === clientId || isPrivilegedFinanceUser(request)) {
return;
}
throw new AppError(403, 'FORBIDDEN', 'Cannot access another client\'s finance basket');
}
// Initialize server
async function initializeServer(): Promise<void> {
// Register Swagger
const swaggerUrl = env.SWAGGER_SERVER_URL || (env.NODE_ENV === 'development' ? 'http://localhost:4003' : undefined);
function resolveClientScope(request: FastifyRequest, clientId?: string): string | undefined {
if (clientId) {
assertClientAccess(request, clientId);
return clientId;
}
return isPrivilegedFinanceUser(request) ? undefined : request.user?.id;
}
function getAccessibleBasket(
request: FastifyRequest,
store: JewelryBoxStore,
basketId: string,
): BasketMandate {
const basket = store.getBasket(basketId);
if (!basket) {
throw new AppError(404, 'NOT_FOUND', 'Basket not found');
}
assertClientAccess(request, basket.clientId);
return basket;
}
function createPaymentGateway(): StripePaymentGateway | null {
try {
return env.PAYMENT_GATEWAY_API_KEY ? new StripePaymentGateway() : null;
} catch (error) {
logger.warn({ err: error }, 'Payment gateway not configured');
return null;
}
}
async function safeDbHealthCheck(): Promise<boolean> {
try {
return await dbHealthCheck();
} catch (error) {
logger.warn({ err: error }, 'Database health check failed');
return false;
}
}
async function registerDocs(server: any, disableSwagger = false): Promise<void> {
if (disableSwagger) {
return;
}
const swaggerUrl =
env.SWAGGER_SERVER_URL || (env.NODE_ENV === 'development' ? 'http://localhost:4003' : undefined);
if (!swaggerUrl) {
logger.warn('SWAGGER_SERVER_URL not set, Swagger documentation will not be available');
} else {
await server.register(fastifySwagger, {
openapi: {
info: {
title: 'Finance Service API',
description: 'Payments, ledgers, rate models, and invoicing',
version: '1.0.0',
},
servers: [
{
url: swaggerUrl,
description: env.NODE_ENV || 'Development server',
},
],
},
});
await server.register(fastifySwaggerUI, {
routePrefix: '/docs',
});
return;
}
await registerSecurityPlugins(server as any);
addCorrelationId(server as any);
addRequestLogging(server as any);
server.setErrorHandler(errorHandler as any);
await server.register(fastifySwagger, {
openapi: {
info: {
title: 'Finance Service API',
description: 'Payments, ledgers, and BTC jewelry-box workflows',
version: '1.0.0',
},
servers: [
{
url: swaggerUrl,
description: env.NODE_ENV || 'Development server',
},
],
},
});
await server.register(fastifySwaggerUI, {
routePrefix: '/docs',
});
}
// Health check
server.get(
'/health',
{
schema: {
description: 'Health check endpoint',
tags: ['health'],
response: {
200: {
type: 'object',
properties: {
status: { type: 'string' },
service: { type: 'string' },
database: { type: 'string' },
},
},
},
},
},
async () => {
const dbHealthy = await dbHealthCheck();
return {
status: dbHealthy ? 'ok' : 'degraded',
service: 'finance',
database: dbHealthy ? 'connected' : 'disconnected',
};
}
);
// Ledger operations
server.post(
'/ledger/entry',
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
preHandler: [authenticateJWT as any, requireRole('admin', 'accountant', 'finance') as any],
schema: {
...createBodySchema(CreateLedgerEntrySchema),
description: 'Create a ledger entry',
tags: ['ledger'],
response: {
201: {
type: 'object',
properties: {
entry: {
type: 'object',
function registerHealthRoute(server: any): void {
server.get(
'/health',
{
schema: {
description: 'Health check endpoint',
tags: ['health'],
response: {
200: {
type: 'object',
properties: {
status: { type: 'string' },
service: { type: 'string' },
database: { type: 'string' },
},
},
},
},
},
},
async (request: FastifyRequest, reply: FastifyReply) => {
const body = request.body as {
accountId: string;
type: 'debit' | 'credit';
amount: number;
currency: string;
description?: string;
reference?: string;
};
async () => {
const dbHealthy = await safeDbHealthCheck();
return {
status: dbHealthy ? 'ok' : 'degraded',
service: 'finance',
database: dbHealthy ? 'connected' : 'disconnected',
};
},
);
}
// Save to database
const entry = await createLedgerEntry({
account_id: body.accountId,
type: body.type,
amount: body.amount,
currency: body.currency,
description: body.description,
reference: body.reference,
});
return reply.status(201).send({ entry });
}
);
// Payment processing
server.post(
'/payments',
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
preHandler: [authenticateJWT as any],
schema: {
...createBodySchema(CreatePaymentSchema),
description: 'Process a payment',
tags: ['payments'],
response: {
201: {
function registerLedgerRoute(server: any): void {
server.post(
'/ledger/entry',
{
preHandler: [authenticateJWT as never, requireRole('admin', 'accountant', 'finance') as never],
schema: {
description: 'Create a ledger entry',
tags: ['ledger'],
body: {
type: 'object',
additionalProperties: false,
required: ['accountId', 'type', 'amount', 'currency'],
properties: {
payment: {
type: 'object',
},
accountId: { type: 'string' },
type: { type: 'string', enum: ['debit', 'credit'] },
amount: { type: 'number', exclusiveMinimum: 0 },
currency: { type: 'string', minLength: 3, maxLength: 3 },
description: { type: 'string' },
reference: { type: 'string' },
},
},
},
},
},
async (request: FastifyRequest, reply: FastifyReply) => {
const body = request.body as {
amount: number;
currency: string;
paymentMethod: string;
metadata?: Record<string, string>;
};
async (
request: FastifyRequest<{
Body: {
accountId: string;
type: 'debit' | 'credit';
amount: number;
currency: string;
description?: string;
reference?: string;
};
}>,
reply: FastifyReply,
) => {
const body = request.body;
const entry = await createLedgerEntry({
account_id: body.accountId,
type: body.type,
amount: body.amount,
currency: body.currency,
description: body.description,
reference: body.reference,
});
// Create payment record
return reply.status(201).send({ entry });
},
);
}
function registerPaymentsRoute(server: any, paymentGateway: StripePaymentGateway | null): void {
const handler = async (
request: FastifyRequest<{
Body: {
amount: number;
currency: string;
paymentMethod: string;
metadata?: Record<string, string>;
};
}>,
reply: FastifyReply,
) => {
const body = request.body;
const payment = await createPayment({
amount: body.amount,
currency: body.currency,
@@ -193,53 +238,324 @@ server.post(
payment_method: body.paymentMethod,
});
// Process payment through gateway if available
if (paymentGateway) {
try {
const result = await paymentGateway.processPayment(
body.amount,
body.currency,
body.paymentMethod,
{
payment_id: payment.id,
...body.metadata,
}
);
// Update payment status
const updatedPayment = await updatePaymentStatus(
payment.id,
result.status,
result.transactionId,
result.gatewayResponse
);
return reply.status(201).send({ payment: updatedPayment });
} catch (error) {
logger.error({ err: error, paymentId: payment.id }, 'Payment processing failed');
await updatePaymentStatus(payment.id, 'failed', undefined, { error: String(error) });
throw error;
}
} else {
// No payment gateway configured - return pending status
if (!paymentGateway) {
return reply.status(201).send({ payment });
}
}
);
// Start server
const start = async () => {
try {
const result = await paymentGateway.processPayment(
body.amount,
body.currency,
body.paymentMethod,
{
payment_id: payment.id,
...body.metadata,
},
);
const updatedPayment = await updatePaymentStatus(
payment.id,
result.status,
result.transactionId,
result.gatewayResponse,
);
return reply.status(201).send({ payment: updatedPayment });
} catch (error) {
logger.error({ err: error, paymentId: payment.id }, 'Payment processing failed');
await updatePaymentStatus(payment.id, 'failed', undefined, { error: String(error) });
throw error;
}
};
const options = {
preHandler: [authenticateJWT as never],
schema: {
description: 'Process a payment',
tags: ['payments'],
body: {
type: 'object',
additionalProperties: false,
required: ['amount', 'currency', 'paymentMethod'],
properties: {
amount: { type: 'number', exclusiveMinimum: 0 },
currency: { type: 'string', minLength: 3, maxLength: 3 },
paymentMethod: { type: 'string' },
description: { type: 'string' },
metadata: {
type: 'object',
additionalProperties: { type: 'string' },
},
},
},
},
};
server.post('/payments', options, handler);
server.post('/api/v1/payments', options, handler);
}
function registerJewelryBoxRoutes(server: any, store: JewelryBoxStore): void {
server.post(
'/api/v1/baskets',
{
preHandler: [authenticateJWT as never],
schema: {
description: 'Create a Chain 138 basket mandate for BTC settlement flows',
tags: ['jewelry-box'],
body: {
type: 'object',
additionalProperties: false,
required: ['clientId', 'mandateName', 'chain138VaultAddress', 'allocations'],
properties: {
clientId: { type: 'string', minLength: 1 },
mandateName: { type: 'string', minLength: 1 },
chain138VaultAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' },
baseAssetSymbol: { type: 'string', minLength: 2, maxLength: 16 },
allocations: {
type: 'array',
minItems: 1,
items: {
type: 'object',
additionalProperties: false,
required: ['symbol', 'targetWeightBps'],
properties: {
symbol: { type: 'string', minLength: 2, maxLength: 16 },
targetWeightBps: { type: 'integer', minimum: 1, maximum: 10000 },
routeHint: { type: 'string' },
},
},
},
},
},
},
},
async (
request: FastifyRequest<{ Body: CreateBasketMandate }>,
reply: FastifyReply,
) => {
assertClientAccess(request, request.body.clientId);
const basket = store.createBasket(request.body);
return reply.status(201).send({ basket });
},
);
server.post(
'/api/v1/btc-deposits',
{
preHandler: [authenticateJWT as never],
schema: {
description: 'Create a BTC deposit instruction and associate it with a jewelry-box basket',
tags: ['jewelry-box'],
body: {
type: 'object',
additionalProperties: false,
required: ['clientId', 'chain138VaultAddress'],
properties: {
clientId: { type: 'string', minLength: 1 },
basketId: { type: 'string' },
mandateName: { type: 'string', minLength: 1 },
chain138VaultAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' },
expectedAmountSats: { type: 'integer', minimum: 1 },
clientReference: { type: 'string', minLength: 1 },
allocations: {
type: 'array',
minItems: 1,
items: {
type: 'object',
additionalProperties: false,
required: ['symbol', 'targetWeightBps'],
properties: {
symbol: { type: 'string', minLength: 2, maxLength: 16 },
targetWeightBps: { type: 'integer', minimum: 1, maximum: 10000 },
routeHint: { type: 'string' },
},
},
},
},
},
},
},
async (
request: FastifyRequest<{ Body: CreateBtcDeposit }>,
reply: FastifyReply,
) => {
assertClientAccess(request, request.body.clientId);
if (request.body.basketId) {
const basket = getAccessibleBasket(request, store, request.body.basketId);
if (basket.clientId !== request.body.clientId) {
throw new AppError(400, 'CLIENT_MISMATCH', 'Deposit client does not match basket client');
}
if (basket.chain138VaultAddress !== request.body.chain138VaultAddress) {
throw new AppError(
400,
'VAULT_ADDRESS_MISMATCH',
'Deposit vault address does not match the selected basket',
);
}
}
const result = store.createDeposit(request.body);
return reply.status(201).send(result);
},
);
server.get(
'/api/v1/btc-deposits/:id',
{
preHandler: [authenticateJWT as never],
schema: {
description: 'Get BTC deposit status',
tags: ['jewelry-box'],
},
},
async (
request: FastifyRequest<{ Params: { id: string } }>,
) => {
const deposit = store.getDeposit(request.params.id);
if (!deposit) {
throw new AppError(404, 'NOT_FOUND', 'Deposit not found');
}
assertClientAccess(request, deposit.clientId);
return { deposit };
},
);
server.get(
'/api/v1/holdings',
{
preHandler: [authenticateJWT as never],
schema: {
description: 'List basket holdings derived from underlying allocations',
tags: ['jewelry-box'],
},
},
async (
request: FastifyRequest<{ Querystring: { clientId?: string; basketId?: string } }>,
) => {
if (request.query.basketId) {
const basket = getAccessibleBasket(request, store, request.query.basketId);
if (request.query.clientId && basket.clientId !== request.query.clientId) {
throw new AppError(400, 'CLIENT_MISMATCH', 'Requested client does not own the selected basket');
}
}
const holdings = store.listHoldings({
...request.query,
clientId: resolveClientScope(request, request.query.clientId),
});
return { holdings };
},
);
server.get(
'/api/v1/rebalances',
{
preHandler: [authenticateJWT as never],
schema: {
description: 'List planned or queued rebalances for jewelry-box baskets',
tags: ['jewelry-box'],
},
},
async (
request: FastifyRequest<{ Querystring: { clientId?: string; basketId?: string } }>,
) => {
if (request.query.basketId) {
const basket = getAccessibleBasket(request, store, request.query.basketId);
if (request.query.clientId && basket.clientId !== request.query.clientId) {
throw new AppError(400, 'CLIENT_MISMATCH', 'Requested client does not own the selected basket');
}
}
const rebalances = store.listRebalances({
...request.query,
clientId: resolveClientScope(request, request.query.clientId),
});
return { rebalances };
},
);
server.post(
'/api/v1/withdrawals/bridge',
{
preHandler: [authenticateJWT as never],
schema: {
description: 'Request a public-chain bridge withdrawal from a jewelry-box basket',
tags: ['jewelry-box'],
body: {
type: 'object',
additionalProperties: false,
required: ['clientId', 'basketId', 'destinationChainId', 'destinationAddress', 'amount'],
properties: {
clientId: { type: 'string', minLength: 1 },
basketId: { type: 'string', minLength: 1 },
sourceSymbol: { type: 'string', minLength: 2, maxLength: 16 },
destinationSymbol: { type: 'string', minLength: 2, maxLength: 16 },
destinationChainId: { type: 'integer', minimum: 1 },
destinationAddress: { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' },
amount: { type: 'string', minLength: 1 },
},
},
},
},
async (
request: FastifyRequest<{ Body: CreateBridgeWithdrawal }>,
reply: FastifyReply,
) => {
const basket = getAccessibleBasket(request, store, request.body.basketId);
if (basket.clientId !== request.body.clientId) {
throw new AppError(400, 'CLIENT_MISMATCH', 'Withdrawal client does not match basket client');
}
const withdrawal = store.createBridgeWithdrawal(
createDefaultBridgeWithdrawal(request.body as CreateBridgeWithdrawal),
);
return reply.status(201).send({ withdrawal });
},
);
}
export async function createServer(options: CreateServerOptions = {}): Promise<FastifyInstance> {
if (env.DATABASE_URL) {
getPool({ connectionString: env.DATABASE_URL });
}
const server: any = Fastify({
logger: logger as any,
requestIdLogLabel: 'requestId',
disableRequestLogging: false,
});
await registerDocs(server, options.disableSwagger);
await registerSecurityPlugins(server as any);
addCorrelationId(server);
addRequestLogging(server);
server.setErrorHandler(errorHandler as any);
registerHealthRoute(server);
registerLedgerRoute(server);
registerPaymentsRoute(server, createPaymentGateway());
registerJewelryBoxRoutes(server, options.jewelryBoxStore || new JewelryBoxStore());
return server as FastifyInstance;
}
export async function start(): Promise<void> {
try {
await initializeServer();
const env = getEnv();
const server = await createServer();
const port = env.PORT || 4003;
await server.listen({ port, host: '0.0.0.0' });
logger.info({ port }, 'Finance service listening');
} catch (err) {
logger.error({ err }, 'Failed to start server');
} catch (error) {
logger.error({ err: error }, 'Failed to start server');
process.exit(1);
}
};
}
start();
const isDirectExecution = process.argv[1] === fileURLToPath(import.meta.url);
if (isDirectExecution) {
void start();
}

View File

@@ -0,0 +1,296 @@
import { randomUUID } from 'crypto';
import type {
BasketAllocation,
BasketMandate,
BtcDeposit,
BridgeWithdrawal,
CreateBasketMandate,
CreateBridgeWithdrawal,
CreateBtcDeposit,
Holding,
Rebalance,
} from '@the-order/schemas';
const BTC_CONFIRMATIONS_REQUIRED = 6;
const DEFAULT_ALLOCATION: BasketAllocation = { symbol: 'cBTC', targetWeightBps: 10_000 };
const BTC_BRIDGE_SYMBOL = 'cWBTC';
const BECH32_ALPHABET = '023456789acdefghjklmnpqrstuvwxyz';
function nowIso(): string {
return new Date().toISOString();
}
function sumWeights(allocations: BasketAllocation[]): number {
return allocations.reduce((total, allocation) => total + allocation.targetWeightBps, 0);
}
function toPseudoBitcoinAddress(seed: string): string {
const normalized = seed.replace(/[^a-f0-9]/gi, '').toLowerCase() || 'cb7c138';
let out = 'bc1q';
for (let index = 0; index < 32; index += 1) {
const source = normalized[index % normalized.length] ?? '0';
const value = parseInt(source, 16);
out += Number.isNaN(value) ? 'q' : BECH32_ALPHABET[value];
}
return out;
}
function clone<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
export class JewelryBoxStore {
private readonly baskets = new Map<string, BasketMandate>();
private readonly deposits = new Map<string, BtcDeposit>();
private readonly rebalances = new Map<string, Rebalance>();
private readonly withdrawals = new Map<string, BridgeWithdrawal>();
createBasket(input: CreateBasketMandate): BasketMandate {
const allocations = input.allocations.length > 0 ? input.allocations : [DEFAULT_ALLOCATION];
if (sumWeights(allocations) !== 10_000) {
throw new Error('Basket allocations must sum to exactly 10,000 bps');
}
const timestamp = nowIso();
const basket: BasketMandate = {
id: randomUUID(),
clientId: input.clientId,
mandateName: input.mandateName,
chain138VaultAddress: input.chain138VaultAddress,
baseAssetSymbol: input.baseAssetSymbol || 'cBTC',
status: 'active',
allocations: clone(allocations),
createdAt: timestamp,
updatedAt: timestamp,
};
this.baskets.set(basket.id, basket);
this.seedRebalancePlan(basket);
return clone(basket);
}
getBasket(id: string): BasketMandate | null {
const basket = this.baskets.get(id);
return basket ? clone(basket) : null;
}
createDeposit(input: CreateBtcDeposit): { deposit: BtcDeposit; basket: BasketMandate; createdBasket: boolean } {
let basket = input.basketId ? this.baskets.get(input.basketId) ?? null : null;
let createdBasket = false;
if (input.basketId && !basket) {
throw new Error(`Unknown basket: ${input.basketId}`);
}
if (!basket) {
basket = this.createBasket({
clientId: input.clientId,
mandateName: input.mandateName || `BTC jewelry box ${input.clientId}`,
chain138VaultAddress: input.chain138VaultAddress,
allocations: input.allocations && input.allocations.length > 0 ? input.allocations : [DEFAULT_ALLOCATION],
baseAssetSymbol: 'cBTC',
});
createdBasket = true;
} else {
if (basket.clientId !== input.clientId) {
throw new Error('Basket client does not match deposit client');
}
if (basket.chain138VaultAddress !== input.chain138VaultAddress) {
throw new Error('Basket vault address does not match deposit vault address');
}
}
const timestamp = nowIso();
const depositId = randomUUID();
const deposit: BtcDeposit = {
id: depositId,
clientId: input.clientId,
basketId: basket.id,
chain138VaultAddress: basket.chain138VaultAddress,
depositAddress: toPseudoBitcoinAddress(depositId),
expectedAmountSats: input.expectedAmountSats,
confirmationsRequired: BTC_CONFIRMATIONS_REQUIRED,
currentConfirmations: 0,
status: 'instruction_created',
createdAt: timestamp,
updatedAt: timestamp,
};
this.deposits.set(deposit.id, deposit);
return { deposit: clone(deposit), basket: clone(basket), createdBasket };
}
getDeposit(id: string): BtcDeposit | null {
const deposit = this.deposits.get(id);
return deposit ? clone(deposit) : null;
}
listHoldings(filters: { clientId?: string; basketId?: string }): Holding[] {
const baskets = Array.from(this.baskets.values()).filter((basket) => {
if (filters.clientId && basket.clientId !== filters.clientId) {
return false;
}
if (filters.basketId && basket.id !== filters.basketId) {
return false;
}
return true;
});
return baskets.flatMap((basket) => {
const fundedSats = Array.from(this.deposits.values())
.filter((deposit) => deposit.basketId === basket.id && deposit.status === 'minted')
.reduce((total, deposit) => total + (deposit.expectedAmountSats ?? 0), 0);
return basket.allocations.map((allocation: BasketAllocation) => ({
clientId: basket.clientId,
basketId: basket.id,
symbol: allocation.symbol,
allocationWeightBps: allocation.targetWeightBps,
bookValueSats: Math.floor((fundedSats * allocation.targetWeightBps) / 10_000),
status: fundedSats > 0 ? 'funded' : 'pending_funding',
updatedAt: basket.updatedAt,
}));
});
}
listRebalances(filters: { clientId?: string; basketId?: string }): Rebalance[] {
return Array.from(this.rebalances.values())
.filter((rebalance) => {
if (filters.clientId && rebalance.clientId !== filters.clientId) {
return false;
}
if (filters.basketId && rebalance.basketId !== filters.basketId) {
return false;
}
return true;
})
.map((rebalance) => clone(rebalance));
}
createBridgeWithdrawal(input: CreateBridgeWithdrawal): BridgeWithdrawal {
if (!this.baskets.has(input.basketId)) {
throw new Error(`Unknown basket: ${input.basketId}`);
}
const timestamp = nowIso();
const withdrawal: BridgeWithdrawal = {
id: randomUUID(),
clientId: input.clientId,
basketId: input.basketId,
sourceSymbol: input.sourceSymbol || 'cBTC',
destinationSymbol: input.destinationSymbol || BTC_BRIDGE_SYMBOL,
destinationChainId: input.destinationChainId,
destinationAddress: input.destinationAddress,
amount: input.amount,
status: 'pending',
createdAt: timestamp,
updatedAt: timestamp,
};
this.withdrawals.set(withdrawal.id, withdrawal);
return clone(withdrawal);
}
markDepositObserved(
depositId: string,
observedTxId: string,
confirmations: number,
expectedAmountSats?: number,
): BtcDeposit {
const deposit = this.deposits.get(depositId);
if (!deposit) {
throw new Error(`Unknown BTC deposit: ${depositId}`);
}
const nextConfirmations = Math.max(0, confirmations);
deposit.observedTxId = observedTxId;
deposit.currentConfirmations = nextConfirmations;
if (expectedAmountSats !== undefined) {
deposit.expectedAmountSats = expectedAmountSats;
}
deposit.status =
nextConfirmations >= deposit.confirmationsRequired ? 'confirmed' : 'pending_confirmations';
deposit.updatedAt = nowIso();
return clone(deposit);
}
markDepositMinted(depositId: string): BtcDeposit {
const deposit = this.deposits.get(depositId);
if (!deposit) {
throw new Error(`Unknown BTC deposit: ${depositId}`);
}
deposit.currentConfirmations = Math.max(deposit.currentConfirmations, deposit.confirmationsRequired);
deposit.status = 'minted';
deposit.updatedAt = nowIso();
const basket = this.baskets.get(deposit.basketId);
if (basket) {
basket.updatedAt = deposit.updatedAt;
if (basket.allocations.some((allocation: BasketAllocation) => allocation.symbol !== 'cBTC')) {
this.promoteRebalancePlan(basket);
}
}
return clone(deposit);
}
freezeDeposit(depositId: string, reason: string): BtcDeposit {
const deposit = this.deposits.get(depositId);
if (!deposit) {
throw new Error(`Unknown BTC deposit: ${depositId}`);
}
deposit.status = 'frozen';
deposit.freezeReason = reason;
deposit.updatedAt = nowIso();
return clone(deposit);
}
private seedRebalancePlan(basket: BasketMandate): void {
const targetSymbols = basket.allocations
.map((allocation: BasketAllocation) => allocation.symbol)
.filter((symbol: string) => symbol !== 'cBTC');
if (targetSymbols.length === 0) {
return;
}
const timestamp = nowIso();
const rebalance: Rebalance = {
id: randomUUID(),
clientId: basket.clientId,
basketId: basket.id,
sourceSymbol: 'cBTC',
targetSymbols,
reason: 'Awaiting funded BTC deposit before Chain 138 rebalance',
status: 'planned',
createdAt: timestamp,
updatedAt: timestamp,
};
this.rebalances.set(rebalance.id, rebalance);
}
private promoteRebalancePlan(basket: BasketMandate): void {
for (const rebalance of this.rebalances.values()) {
if (rebalance.basketId === basket.id && rebalance.status === 'planned') {
rebalance.status = 'queued';
rebalance.updatedAt = nowIso();
}
}
}
}
export function createDefaultBridgeWithdrawal(
input: Omit<CreateBridgeWithdrawal, 'destinationSymbol' | 'sourceSymbol'> & {
sourceSymbol?: string;
destinationSymbol?: string;
},
): CreateBridgeWithdrawal {
return {
...input,
sourceSymbol: input.sourceSymbol || 'cBTC',
destinationSymbol: input.destinationSymbol || BTC_BRIDGE_SYMBOL,
};
}

View File

@@ -1,45 +1,56 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { FastifyInstance } from 'fastify';
import { createServer } from '../src/index';
import { describe, expect, it } from 'vitest';
import { JewelryBoxStore } from '../src/jewelry-box-store';
describe('Finance Service', () => {
let server: FastifyInstance;
describe('JewelryBoxStore', () => {
it('creates queued rebalance work only for non-cBTC allocations', () => {
const store = new JewelryBoxStore();
const basket = store.createBasket({
clientId: 'client-1',
mandateName: 'Multi-asset jewelry box',
chain138VaultAddress: '0x4444444444444444444444444444444444444444',
allocations: [
{ symbol: 'cBTC', targetWeightBps: 2500 },
{ symbol: 'cUSDC', targetWeightBps: 5000 },
{ symbol: 'cXAUC', targetWeightBps: 2500 },
],
baseAssetSymbol: 'cBTC',
});
beforeEach(async () => {
server = await createServer();
await server.ready();
});
afterEach(async () => {
await server.close();
});
describe('Health Check', () => {
it('should return 200 on health check', async () => {
const response = await server.inject({
method: 'GET',
url: '/health',
});
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({
status: 'ok',
});
const rebalances = store.listRebalances({ basketId: basket.id });
expect(rebalances).toHaveLength(1);
expect(rebalances[0]).toMatchObject({
sourceSymbol: 'cBTC',
targetSymbols: ['cUSDC', 'cXAUC'],
status: 'planned',
});
});
describe('Payment Processing', () => {
it('should validate payment request schema', async () => {
const response = await server.inject({
method: 'POST',
url: '/api/v1/payments',
payload: {
// Invalid payload to test validation
},
});
expect(response.statusCode).toBe(400);
it('promotes rebalance plans once a deposit is marked minted', () => {
const store = new JewelryBoxStore();
const { basket, deposit } = store.createDeposit({
clientId: 'client-2',
chain138VaultAddress: '0x5555555555555555555555555555555555555555',
allocations: [
{ symbol: 'cBTC', targetWeightBps: 3000 },
{ symbol: 'cUSDT', targetWeightBps: 7000 },
],
expectedAmountSats: 300000000,
});
store.markDepositObserved(deposit.id, 'btc-tx-1', 6, 300000000);
store.markDepositMinted(deposit.id);
const updatedDeposit = store.getDeposit(deposit.id);
expect(updatedDeposit?.status).toBe('minted');
const holdings = store.listHoldings({ basketId: basket.id });
expect(holdings[1]).toMatchObject({
symbol: 'cUSDT',
bookValueSats: 210000000,
status: 'funded',
});
const rebalances = store.listRebalances({ basketId: basket.id });
expect(rebalances[0]?.status).toBe('queued');
});
});

View File

@@ -0,0 +1,18 @@
import path from 'path';
const root = '/home/intlc/projects/proxmox/the-order';
export default {
resolve: {
alias: {
'@the-order/auth': path.join(root, 'packages/auth/src/index.ts'),
'@the-order/shared': path.join(root, 'packages/shared/src/index.ts'),
'@the-order/schemas': path.join(root, 'packages/schemas/src/index.ts'),
'@the-order/database': path.join(root, 'packages/database/src/index.ts'),
'@the-order/payment-gateway': path.join(root, 'packages/payment-gateway/src/index.ts'),
},
},
test: {
environment: 'node',
},
};