mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
feat: ( payment ) create payment microservice
This commit is contained in:
21
apps/edr-payment-api/src/config/app.config.ts
Normal file
21
apps/edr-payment-api/src/config/app.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("app", () => ({
|
||||
port: parseInt(process.env.PORT ?? "3003", 10),
|
||||
/**
|
||||
* Shared secret for service-to-service auth (apps -> /payments/*, payment -> mark-paid).
|
||||
* Required in production; in development an empty value disables the guard with a warning.
|
||||
* TODO: integrate @tria-plc IAM / mTLS as the long-term mechanism (docs/payment-service §14).
|
||||
*/
|
||||
serviceAuthToken: process.env.SERVICE_AUTH_TOKEN ?? "",
|
||||
reconciliation: {
|
||||
/** How often the stale-intent sweep runs. */
|
||||
sweepIntervalMs: parseInt(
|
||||
process.env.RECONCILE_SWEEP_INTERVAL_MS ?? "60000",
|
||||
10,
|
||||
),
|
||||
/** An intent is "stale" when non-terminal and untouched for this long. */
|
||||
staleAfterMs: parseInt(process.env.RECONCILE_STALE_AFTER_MS ?? "60000", 10),
|
||||
batchSize: parseInt(process.env.RECONCILE_BATCH_SIZE ?? "20", 10),
|
||||
},
|
||||
}));
|
||||
9
apps/edr-payment-api/src/config/card.config.ts
Normal file
9
apps/edr-payment-api/src/config/card.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("card", () => ({
|
||||
baseUrl: process.env.CARD_BASE_URL || "",
|
||||
apiKey: process.env.CARD_API_KEY || "",
|
||||
webhookSecret: process.env.CARD_WEBHOOK_SECRET || "",
|
||||
webhookUrl: process.env.CARD_WEBHOOK_URL || "",
|
||||
returnUrl: process.env.CARD_RETURN_URL || "",
|
||||
}));
|
||||
9
apps/edr-payment-api/src/config/cbe.config.ts
Normal file
9
apps/edr-payment-api/src/config/cbe.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("cbe", () => ({
|
||||
baseUrl: process.env.CBE_BASE_URL || "",
|
||||
merchantId: process.env.CBE_MERCHANT_ID || "",
|
||||
secretKey: process.env.CBE_SECRET_KEY || "",
|
||||
notifyUrl: process.env.CBE_NOTIFY_URL || "",
|
||||
returnUrl: process.env.CBE_RETURN_URL || "",
|
||||
}));
|
||||
36
apps/edr-payment-api/src/config/database.config.ts
Normal file
36
apps/edr-payment-api/src/config/database.config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
|
||||
import { DataSourceOptions } from "typeorm";
|
||||
|
||||
/**
|
||||
* Shared connection options for the Nest TypeORM module and the standalone DataSource
|
||||
* (migration CLI). Payment tables live in the SAME Postgres database as the domain system
|
||||
* (edr_database by default) but in the dedicated `edr_payment` schema; logical ownership is
|
||||
* enforced with a dedicated DB user in non-dev environments (grants only on this schema).
|
||||
*/
|
||||
export function buildDataSourceOptions(): DataSourceOptions {
|
||||
return {
|
||||
type: "postgres",
|
||||
host: process.env.DB_HOST ?? "localhost",
|
||||
port: parseInt(process.env.DB_PORT ?? "5432", 10),
|
||||
username: process.env.DB_USER ?? "edr",
|
||||
password: process.env.DB_PASSWORD ?? "",
|
||||
database: process.env.DB_NAME ?? "edr_database",
|
||||
schema: process.env.DB_SCHEMA ?? "edr_payment",
|
||||
entities: [__dirname + "/../**/*.entity.{ts,js}"],
|
||||
migrations: [__dirname + "/../migrations/*.{ts,js}"],
|
||||
// Schema changes go through migrations only — never synchronize (house rule).
|
||||
synchronize: false,
|
||||
logging: process.env.NODE_ENV === "development",
|
||||
};
|
||||
}
|
||||
|
||||
export default registerAs(
|
||||
"database",
|
||||
(): TypeOrmModuleOptions => ({
|
||||
...buildDataSourceOptions(),
|
||||
autoLoadEntities: true,
|
||||
// Run pending migrations on boot (main.ts ensures the database/schema exist first).
|
||||
migrationsRun: true,
|
||||
}),
|
||||
);
|
||||
10
apps/edr-payment-api/src/config/dmoney.config.ts
Normal file
10
apps/edr-payment-api/src/config/dmoney.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("dmoney", () => ({
|
||||
baseUrl: process.env.DMONEY_BASE_URL ?? "",
|
||||
appId: process.env.DMONEY_APP_ID ?? "",
|
||||
appSecret: process.env.DMONEY_APP_SECRET ?? "",
|
||||
publicKey: process.env.DMONEY_PUBLIC_KEY ?? "",
|
||||
privateKey: process.env.DMONEY_PRIVATE_KEY ?? "",
|
||||
notifyUrl: process.env.DMONEY_NOTIFY_URL ?? "",
|
||||
}));
|
||||
9
apps/edr-payment-api/src/config/ebirr.config.ts
Normal file
9
apps/edr-payment-api/src/config/ebirr.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("ebirr", () => ({
|
||||
baseUrl: process.env.EBIRR_BASE_URL || "",
|
||||
merchantCode: process.env.EBIRR_MERCHANT_CODE || "",
|
||||
secretKey: process.env.EBIRR_SECRET_KEY || "",
|
||||
notifyUrl: process.env.EBIRR_NOTIFY_URL || "",
|
||||
returnUrl: process.env.EBIRR_RETURN_URL || "",
|
||||
}));
|
||||
52
apps/edr-payment-api/src/config/ensure-schema.ts
Normal file
52
apps/edr-payment-api/src/config/ensure-schema.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Client } from "pg";
|
||||
|
||||
const IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
|
||||
|
||||
function connectionEnv() {
|
||||
return {
|
||||
host: process.env.DB_HOST ?? "localhost",
|
||||
port: parseInt(process.env.DB_PORT ?? "5432", 10),
|
||||
user: process.env.DB_USER ?? "edr",
|
||||
password: process.env.DB_PASSWORD ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dev/bootstrap convenience: make sure the `edr_payment` schema exists in the shared
|
||||
* database before TypeORM initializes (the migrations table itself lives in the schema, so
|
||||
* migrations cannot create it). In production the schema/grants are provisioned out-of-band
|
||||
* by ops; this is then a no-op.
|
||||
*/
|
||||
export async function ensurePaymentSchema(): Promise<void> {
|
||||
const database = process.env.DB_NAME ?? "edr_database";
|
||||
const schema = process.env.DB_SCHEMA ?? "edr_payment";
|
||||
if (!IDENTIFIER.test(database) || !IDENTIFIER.test(schema)) {
|
||||
throw new Error(
|
||||
`Invalid DB_NAME/DB_SCHEMA identifier: ${database}/${schema}`,
|
||||
);
|
||||
}
|
||||
|
||||
let client = new Client({ ...connectionEnv(), database });
|
||||
try {
|
||||
await client.connect();
|
||||
} catch (err) {
|
||||
// 3D000 = database does not exist — create it from the maintenance DB, then reconnect.
|
||||
if ((err as { code?: string }).code !== "3D000") throw err;
|
||||
await client.end().catch(() => undefined);
|
||||
const admin = new Client({ ...connectionEnv(), database: "postgres" });
|
||||
await admin.connect();
|
||||
try {
|
||||
await admin.query(`CREATE DATABASE "${database}"`);
|
||||
} finally {
|
||||
await admin.end();
|
||||
}
|
||||
client = new Client({ ...connectionEnv(), database });
|
||||
await client.connect();
|
||||
}
|
||||
|
||||
try {
|
||||
await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}"`);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
15
apps/edr-payment-api/src/config/notifier.config.ts
Normal file
15
apps/edr-payment-api/src/config/notifier.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("notifier", () => ({
|
||||
/** mark-paid callback URL per owning service (PaymentService discriminator routes here). */
|
||||
passengerUrl:
|
||||
process.env.PAYMENT_NOTIFY_PASSENGER_URL ??
|
||||
"http://localhost:3002/internal/payments/mark-paid",
|
||||
freightUrl:
|
||||
process.env.PAYMENT_NOTIFY_FREIGHT_URL ??
|
||||
"http://localhost:3001/internal/payments/mark-paid",
|
||||
relayIntervalMs: parseInt(process.env.OUTBOX_RELAY_INTERVAL_MS ?? "5000", 10),
|
||||
maxAttempts: parseInt(process.env.OUTBOX_MAX_ATTEMPTS ?? "10", 10),
|
||||
httpTimeoutMs: parseInt(process.env.NOTIFY_HTTP_TIMEOUT_MS ?? "10000", 10),
|
||||
relayBatchSize: parseInt(process.env.OUTBOX_RELAY_BATCH_SIZE ?? "20", 10),
|
||||
}));
|
||||
16
apps/edr-payment-api/src/config/telebirr.config.ts
Normal file
16
apps/edr-payment-api/src/config/telebirr.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("telebirr", () => ({
|
||||
baseUrl: process.env.TELEBIRR_BASE_URL ?? "",
|
||||
webBaseUrl: process.env.TELEBIRR_WEB_BASE_URL ?? "",
|
||||
fabricAppId: process.env.TELEBIRR_FABRIC_APP_ID ?? "",
|
||||
appSecret: process.env.TELEBIRR_APP_SECRET ?? "",
|
||||
merchantAppId: process.env.TELEBIRR_MERCHANT_APP_ID ?? "",
|
||||
merchantCode: process.env.TELEBIRR_MERCHANT_CODE ?? "",
|
||||
notifyUrl: process.env.TELEBIRR_NOTIFY_URL ?? "",
|
||||
returnUrl: process.env.TELEBIRR_RETURN_URL ?? "",
|
||||
timeoutExpress: process.env.TELEBIRR_TIMEOUT_EXPRESS ?? "15m",
|
||||
privateKey: process.env.TELEBIRR_PRIVATE_KEY ?? "",
|
||||
publicKey: process.env.TELEBIRR_PUBLIC_KEY ?? "",
|
||||
insecureTls: process.env.TELEBIRR_INSECURE_TLS === "true",
|
||||
}));
|
||||
27
apps/edr-payment-api/src/config/waafi.config.ts
Normal file
27
apps/edr-payment-api/src/config/waafi.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export default registerAs("waafi", () => ({
|
||||
// `/asm` is appended in the provider; use sandbox by default, switch to
|
||||
// https://api.waafipay.net in production.
|
||||
baseUrl: process.env.WAAFI_BASE_URL ?? "https://sandbox.waafipay.net",
|
||||
// HPP credentials (Hosted Payment Page family).
|
||||
merchantUid: process.env.WAAFI_MERCHANT_UID ?? "",
|
||||
storeId: process.env.WAAFI_STORE_ID ?? "",
|
||||
hppKey: process.env.WAAFI_HPP_KEY ?? "",
|
||||
// HMAC secret returned once by WEBHOOK_REGISTER; verifies inbound webhooks.
|
||||
webhookSecret: process.env.WAAFI_WEBHOOK_SECRET ?? "",
|
||||
// Wallet payment method (EVC/ZAAD/Sahal) — MWALLET_ACCOUNT requires the payer phone up front.
|
||||
paymentMethod: process.env.WAAFI_PAYMENT_METHOD ?? "MWALLET_ACCOUNT",
|
||||
// Waafi has no ETB; when set this overrides the asserted currency (USD/DJF/SLSH).
|
||||
currency: process.env.WAAFI_CURRENCY ?? "DJF",
|
||||
// Browser redirect targets after the hosted page completes/fails (UX only; webhook is source of truth).
|
||||
successUrl: process.env.WAAFI_HPP_SUCCESS_URL ?? "",
|
||||
failureUrl: process.env.WAAFI_HPP_FAILURE_URL ?? "",
|
||||
// Callback data format: 1 = POST, 2 = GET, 4 = Result Token.
|
||||
respDataFormat: Number(process.env.WAAFI_HPP_RESP_FORMAT ?? "1"),
|
||||
// Registered webhook URL (reference only; registration is performed out-of-band).
|
||||
notifyUrl: process.env.WAAFI_NOTIFY_URL ?? "",
|
||||
// DEV ONLY: disable TLS cert verification. The Waafi sandbox serves a *.waafi.com cert that
|
||||
// does not match sandbox.waafipay.net (ERR_TLS_CERT_ALTNAME_INVALID). Never enable in prod.
|
||||
insecureTls: process.env.WAAFI_INSECURE_TLS === "true",
|
||||
}));
|
||||
Reference in New Issue
Block a user