@tomei/payment
v1.0.0-test.3
Published
Centralized payment system domain package — entities, types, and table schema definitions
Readme
@tomei/payment
Pure domain package for the Centralized Payment System (CPS). Contains domain models, enums, types, and ORM-agnostic table schema interfaces. No database connections, no ORM, no repositories — those belong in the application layer.
What's inside
| Export path | Contents |
|---|---|
| @tomei/payment | Domain models, enums, TypeScript types |
| @tomei/payment/schema | Plain TypeScript row interfaces for all tables |
Installation
npm install @tomei/paymentDomain Models
Merchant
import { Merchant, MerchantStatus } from '@tomei/payment';
const merchant = Merchant.create({
merchantCode: 'MCH-00001', // MCH-XXXXX format
name: 'Acme Sdn Bhd',
email: '[email protected]',
phone: '0123456789',
address: '123 Jalan Maju, KL',
registrationNo: 'SA0012345-A',
});
merchant.attachAccountingSystem({ accSystemType: 'bukku', contactId: 'c_001', accountId: 'a_001' });
merchant.activate(); // guards: must be PENDING + hasAccountingSystem()
merchant.suspend();
merchant.isActive(); // booleanMerchantFee
Fee rules are versioned — never mutate an active row, call expire() then insert a new one.
import { MerchantFee, FeeCategory, FeeType } from '@tomei/payment';
// id supplied by app layer (cuid2, UUID, ULID, etc.)
const fee = MerchantFee.create({
id: 'fee_abc123',
merchantCode: 'MCH-00001',
feeCategory: FeeCategory.TRANSACTION_FEE,
feeType: FeeType.PERCENTAGE,
feeRate: 1.5,
});
fee.calculateAmount(100); // → 1.50
fee.expire(); // mark old rule inactive before inserting new one
fee.isActive(); // booleanFeeCategory: TRANSACTION_FEE, SETUP_FEE, MONTHLY_FEEFeeType: PERCENTAGE, FLAT
MerchantPaymentMethod
import { MerchantPaymentMethod, PaymentMethodCode } from '@tomei/payment';
const method = MerchantPaymentMethod.create({
merchantCode: 'MCH-00001',
methodCode: PaymentMethodCode.FPX,
});
method.enable();
method.disable();
method.isEnabled();PaymentMethodCode: fpx, qr, credit_card, e_wallet
MerchantBankAccount
import { MerchantBankAccount } from '@tomei/payment';
const account = MerchantBankAccount.create({
id: 'bank_abc123',
merchantCode: 'MCH-00001',
bankCode: 'MBBEMYKL',
bankName: 'Maybank',
accountNo: '1234567890',
accountHolderName: 'Acme Sdn Bhd',
isPrimary: true,
});
account.markAsPrimary();
account.unmarkAsPrimary();MerchantDisbursementSetting
import { MerchantDisbursementSetting, DisbursementFrequency } from '@tomei/payment';
const setting = MerchantDisbursementSetting.create({
merchantCode: 'MCH-00001',
disbursementFreq: DisbursementFrequency.WEEKLY,
isAutoDisburseEnabled: true,
minDisburseAmount: 50,
disbursementDayOfWeek: 5, // Friday
timezone: 'Asia/Kuala_Lumpur',
});
setting.isDueToday(new Date());
setting.calculateReadyAt(new Date());
setting.recordDisbursement(new Date());DisbursementFrequency: DAILY, WEEKLY, MONTHLY
MerchantApiKey
The raw secret is returned exactly once and never stored. The app layer passes id; credentials are generated internally using Node's built-in crypto.
import { MerchantApiKey } from '@tomei/payment';
const { entity, rawSecret } = MerchantApiKey.generate({
id: 'key_abc123',
merchantCode: 'MCH-00001',
prefix: 'cps', // optional, defaults to 'cps' — override per business
label: 'Production',
});
// entity.apiKey → 'cps_key_<hex>' (stored in DB, sent in X-Api-Key header)
// rawSecret → 'cps_secret_<hex>' (return to merchant once, never store raw)
// entity.apiSecretHash → SHA-256 of rawSecret (stored in DB)
entity.deactivate();
entity.recordUsage(new Date());
entity.isActive();Schema Row Interfaces
Use these in your application layer repositories alongside any ORM (Drizzle, Prisma, Kysely, raw SQL, etc.).
import type {
MerchantRow,
MerchantFeeRow,
MerchantPaymentMethodRow,
MerchantBankAccountRow,
MerchantDisbursementSettingRow,
MerchantApiKeyRow,
PaymentMethodRow,
PaymentRow,
PaymentLogRow,
PaymentTransactionRow,
PaymentGatewayRequestRow,
DisbursementQueueRow,
DisbursementBatchRow,
DisbursementTransactionRow,
} from '@tomei/payment/schema';All interfaces use snake_case column names matching the database. DECIMAL columns are typed as string to preserve precision.
Project Structure
payment/
├── src/
│ ├── index.ts # Domain exports
│ ├── models/
│ │ ├── Merchant.ts
│ │ ├── MerchantFee.ts
│ │ ├── MerchantPaymentMethod.ts
│ │ ├── MerchantBankAccount.ts
│ │ ├── MerchantDisbursementSetting.ts
│ │ └── MerchantApiKey.ts
│ ├── schema/
│ │ ├── index.ts # Re-exports all row interfaces
│ │ ├── merchants.schema.ts
│ │ ├── merchant-fees.schema.ts
│ │ ├── merchant-payment-methods.schema.ts
│ │ ├── merchant-bank-accounts.schema.ts
│ │ ├── merchant-disbursement-settings.schema.ts
│ │ ├── merchant-api-keys.schema.ts
│ │ ├── payment-methods.schema.ts
│ │ ├── payments.schema.ts
│ │ ├── payment-logs.schema.ts
│ │ ├── payment-transactions.schema.ts
│ │ ├── payment-gateway-requests.schema.ts
│ │ ├── disbursement-queues.schema.ts
│ │ ├── disbursement-batches.schema.ts
│ │ └── disbursement-transactions.schema.ts
│ └── types/
│ └── Merchant.ts
└── src/__tests__/models/ # 99 unit tests (vitest)Design Principles
- No ORM dependency — schema interfaces are plain TypeScript; the app layer chooses its ORM
- No DB connection — this package has zero runtime infrastructure dependencies
- IDs from the app layer — factory methods accept
idas input; the app layer uses cuid2/UUID/ULID - Versioned fee rules —
expire()+ insert new; never mutate an active fee row - Credential hygiene — raw API secret returned once, never stored; SHA-256 hash stored instead
Development
npm install
npm test # vitest
npm run type-check
npm run build # tsup → dist/