mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
fix: reference type in payment and billing
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Make the payment projection source-agnostic so any domain (not just bookings)
|
||||
* can own a payment intent.
|
||||
*
|
||||
* - `payments.type` enum `('booking')` → `varchar(50)`. It now stores the
|
||||
* invoice SOURCE (e.g. 'booking', 'demurrage'), supplied by the caller, so a
|
||||
* new domain no longer needs an enum migration to write its intents.
|
||||
* - adds `payments.reference_type varchar(40)` — the gateway reference type
|
||||
* (`PaymentReferenceType`) the intent was opened with, so the reconcile/poll
|
||||
* path can query the provider without hardcoding it.
|
||||
*
|
||||
* Matches payment/entities/payment.entity.ts.
|
||||
*/
|
||||
export class MakePaymentsTypeGeneric1821000000004 implements MigrationInterface {
|
||||
name = "MakePaymentsTypeGeneric1821000000004";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.payments ALTER COLUMN type TYPE varchar(50) USING type::text;`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE IF EXISTS freight.payments_type_enum;`);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.payments ADD COLUMN reference_type varchar(40);`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.payments DROP COLUMN IF EXISTS reference_type;`,
|
||||
);
|
||||
|
||||
// Restore the single-value enum. Any non-'booking' rows would block the cast;
|
||||
// collapse them first so the down migration is safe.
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.payments SET type = 'booking' WHERE type <> 'booking';`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE freight.payments_type_enum AS ENUM ('booking');`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.payments ALTER COLUMN type TYPE freight.payments_type_enum USING type::freight.payments_type_enum;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { forwardRef, Inject, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { EventEmitter2 } from "@nestjs/event-emitter";
|
||||
import { Freight } from "@edr/types";
|
||||
import { Freight, PaymentReferenceType } from "@edr/types";
|
||||
import { DataSource, EntityManager, In } from "typeorm";
|
||||
|
||||
import { Invoice } from "./entities/invoice.entity";
|
||||
@@ -417,6 +417,12 @@ export class BillingService {
|
||||
|
||||
const result = await this.payment.initiate({
|
||||
referenceId: sourceId,
|
||||
source: invoice.source,
|
||||
// Gateway reference type derives from the invoice source by convention
|
||||
// (source.toUpperCase() ∈ PaymentReferenceType) — no domain word here, and
|
||||
// the domain never supplies it. New sources add their uppercased value to
|
||||
// the PaymentReferenceType enum.
|
||||
referenceType: invoice.source.toUpperCase() as PaymentReferenceType,
|
||||
orderRef: invoice.invoiceNumber,
|
||||
amountMinor: Math.round(Number(invoice.totalAmount)),
|
||||
currency: invoice.currency,
|
||||
|
||||
@@ -2,7 +2,8 @@ import { BaseEntity, Column, CreateDateColumn, Entity, OneToMany, PrimaryGenerat
|
||||
import { PaymentRefundEntity } from "./payment-refund.entity";
|
||||
|
||||
|
||||
type PaymentType = "booking"
|
||||
/** Invoice source that owns the intent ('booking', 'demurrage', …) — caller-supplied. */
|
||||
type PaymentType = string
|
||||
type PaymentMethod = "telebirr" | "cbe-birr" | "ebirr" | "waafi" | "card" | "dmoney" | "cac-bank"
|
||||
type Currency = "ETB" | "USD"
|
||||
export type PaymentStatus = "action-required" | "processing" | "success" | "failed" | "canceled" | "refunded"
|
||||
@@ -15,9 +16,12 @@ export class PaymentEntity extends BaseEntity {
|
||||
@Column({ type: 'varchar', length: 255, name: "ref_id" })
|
||||
refId!: string
|
||||
|
||||
@Column({ type: "enum", enum: ["booking"] })
|
||||
@Column({ type: "varchar", length: 50 })
|
||||
type!: PaymentType;
|
||||
|
||||
@Column({ type: "varchar", length: 40, nullable: true, name: "reference_type" })
|
||||
referenceType?: string;
|
||||
|
||||
@Column({ type: "enum", enum: ["telebirr", "cbe-birr", "ebirr", "waafi", "card", "dmoney", "cac-bank"] })
|
||||
method!: PaymentMethod
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@ import {
|
||||
export interface InitiateIntentInput {
|
||||
/** Opaque domain reference (booking id, …). */
|
||||
referenceId: string;
|
||||
/** Invoice source that owns the intent ('booking', …) — stored on the projection. */
|
||||
source: string;
|
||||
/** Gateway reference type the intent is opened with (caller's domain decides it). */
|
||||
referenceType: PaymentReferenceType;
|
||||
/** Human-readable order ref shown on provider pages. */
|
||||
orderRef: string;
|
||||
/** Authoritative amount in minor units, computed by the caller. */
|
||||
@@ -194,7 +198,7 @@ export class PaymentService {
|
||||
async initiate(input: InitiateIntentInput): Promise<InitiateIntentResult> {
|
||||
const snapshot = await this.paymentClient.initiate({
|
||||
service: PaymentServiceEnum.FREIGHT,
|
||||
referenceType: PaymentReferenceType.SHIPMENT,
|
||||
referenceType: input.referenceType,
|
||||
referenceId: input.referenceId,
|
||||
orderRef: input.orderRef,
|
||||
amountMinor: input.amountMinor,
|
||||
@@ -240,7 +244,6 @@ export class PaymentService {
|
||||
): Promise<PaymentEntity> {
|
||||
const existing = await this.paymentRepo.findOneBy({
|
||||
refId: input.referenceId,
|
||||
type: "booking",
|
||||
});
|
||||
|
||||
const method: PaymentEntity["method"] =
|
||||
@@ -270,7 +273,8 @@ export class PaymentService {
|
||||
|
||||
return this.paymentRepo.create({
|
||||
refId: input.referenceId,
|
||||
type: "booking",
|
||||
type: input.source,
|
||||
referenceType: input.referenceType,
|
||||
amount: input.amountMinor,
|
||||
currency: input.currency as PaymentEntity["currency"],
|
||||
reason: input.reason ?? `Payment for ${input.orderRef}`,
|
||||
@@ -287,12 +291,12 @@ export class PaymentService {
|
||||
* (the booking id, but this service does not load it).
|
||||
*/
|
||||
async getIntentByBookingId(referenceId: string): Promise<IntentStatusDto> {
|
||||
const local = await this.paymentRepo.findOneBy({ refId: referenceId, type: "booking" });
|
||||
const local = await this.paymentRepo.findOneBy({ refId: referenceId });
|
||||
|
||||
let snapshot: PaymentIntentSnapshot | null = null;
|
||||
try {
|
||||
snapshot = await this.paymentClient.getIntentByReference(
|
||||
PaymentReferenceType.SHIPMENT,
|
||||
(local?.referenceType as PaymentReferenceType) ?? PaymentReferenceType.SHIPMENT,
|
||||
referenceId,
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -423,7 +427,7 @@ export class PaymentService {
|
||||
}
|
||||
|
||||
findBookingById(id: string) {
|
||||
return this.paymentRepo.findOneBy({ refId: id, type: "booking" });
|
||||
return this.paymentRepo.findOneBy({ refId: id });
|
||||
}
|
||||
|
||||
formatIntentResponse(intent: PaymentEntity): InitiateResponseDto {
|
||||
@@ -459,7 +463,7 @@ export class PaymentService {
|
||||
failureMessage?: string;
|
||||
}): Promise<{ processed: boolean; alreadyFinalized?: boolean; reason?: string }> {
|
||||
if (event.eventType === "payment.succeeded") {
|
||||
const intent = await this.paymentRepo.findOneBy({ refId: event.referenceId, type: "booking" });
|
||||
const intent = await this.paymentRepo.findOneBy({ refId: event.referenceId });
|
||||
if (!intent) {
|
||||
return { processed: false, reason: `No local intent for reference ${event.referenceId}` };
|
||||
}
|
||||
@@ -472,7 +476,7 @@ export class PaymentService {
|
||||
}
|
||||
|
||||
if (event.eventType === "payment.failed") {
|
||||
const intent = await this.paymentRepo.findOneBy({ refId: event.referenceId, type: "booking" });
|
||||
const intent = await this.paymentRepo.findOneBy({ refId: event.referenceId });
|
||||
if (!intent) {
|
||||
return { processed: false, reason: `No local intent for reference ${event.referenceId}` };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user