merge conflict fix

This commit is contained in:
hagiye
2026-06-24 12:31:59 +03:00
494 changed files with 21885 additions and 14336 deletions

View File

@@ -4,6 +4,7 @@ import {
Get,
HttpStatus,
Param,
ParseUUIDPipe,
Post,
Query,
Res,
@@ -33,6 +34,14 @@ import {
export class PaymentController {
constructor(private readonly paymentService: PaymentService) { }
@Get("by-company/:companyId/customer-view")
@ApiOperation({ summary: "List payments for a company (customer-view shape, backoffice)" })
findByCompanyCustomerView(
@Param("companyId", ParseUUIDPipe) companyId: string,
) {
return this.paymentService.findByCompanyId(companyId);
}
@Get("summary")
@BookingView()
@ApiOperation({ summary: "Payment count/amount summary for dashboard cards" })

View File

@@ -18,6 +18,7 @@ import { PaymentEventsConsumer } from "./payment-events.consumer";
import { InternalPaymentController } from "./internal-payment.controller";
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.module";
import { DropdownSettingsModule } from "../dropdown-settings/dropdown-settings.module";
import { PaymentWebhookEventEntity } from "./entities/payment-webhook-event.entity";
import { PaymentRefundEntity } from "./entities/payment-refund.entity";
@@ -54,6 +55,7 @@ function rabbitMQImport(): DynamicModule[] {
imports: [
HttpModule.register({ timeout: 10_000 }),
ConfigModule,
DropdownSettingsModule,
forwardRef(() => TrainSchedulingModule),
TypeOrmModule.forFeature([PaymentWebhookEventEntity, PaymentRefundEntity]),
...rabbitMQImport(),

View File

@@ -61,4 +61,56 @@ export class PaymentRepository {
return this.paymentRepo.createQueryBuilder(alias);
}
async findByCompanyId(companyId: string): Promise<{
id: string;
merchantOrderId: string;
bookingReference: string;
amount: number;
currency: string;
method: string;
status: string;
paidAt: Date | null;
createdAt: Date;
}[]> {
const rows: {
id: string;
merchant_order_id: string;
booking_reference: string;
amount: number;
currency: string;
method: string;
status: string;
paid_at: Date | null;
created_at: Date;
}[] = await this.dataSource.query(
`SELECT p.id,
p.merchant_order_id,
b.reference AS booking_reference,
p.amount,
p.currency,
p.method,
p.status,
p.paid_at,
p.created_at
FROM freight.payments p
JOIN freight.bookings b ON b.id = p.ref_id
WHERE b.company_id = $1
AND p.deleted_at IS NULL
AND b.deleted_at IS NULL
ORDER BY p.created_at DESC`,
[companyId],
);
return rows.map((r) => ({
id: r.id,
merchantOrderId: r.merchant_order_id,
bookingReference: r.booking_reference,
amount: Number(r.amount),
currency: r.currency,
method: r.method,
status: r.status,
paidAt: r.paid_at,
createdAt: r.created_at,
}));
}
}

View File

@@ -34,6 +34,11 @@ import {
RefundDto,
} from "./payments.dto";
import { BookingBatchService } from "../train-scheduling/booking-batch.service";
import { DropdownSettingsService } from "../dropdown-settings/dropdown-settings.service";
/** Setting code holding the global ordering window (months) for general contracts. */
const CONTRACT_PERIOD_SETTING_CODE = "general_contract_period";
const DEFAULT_CONTRACT_PERIOD_MONTHS = 3;
const STATUS_MAP: Record<string, ProviderPaymentStatus> = {
"action-required": ProviderPaymentStatus.REQUIRES_ACTION,
@@ -54,8 +59,23 @@ export class PaymentService {
private readonly paymentClient: PaymentClientService,
@Inject(forwardRef(() => BookingBatchService))
private readonly bookingBatchService: BookingBatchService,
private readonly dropdownSettings: DropdownSettingsService,
) { }
/** Configured general-contract ordering window in months (defaults to 3). */
private async contractPeriodMonths(): Promise<number> {
try {
const setting = await this.dropdownSettings.getByCode(
CONTRACT_PERIOD_SETTING_CODE,
);
const months = Number(setting.children?.[0]?.value);
if (Number.isFinite(months) && months > 0) return months;
} catch {
// Setting not seeded — fall back to the default.
}
return DEFAULT_CONTRACT_PERIOD_MONTHS;
}
async getAll(filters: {
search?: string;
status?: string;
@@ -293,15 +313,44 @@ export class PaymentService {
const paidAt = input.paidAt ?? new Date();
// A general contract is paid once, up front; it does NOT enter the train
// queue (nothing has been ordered yet). Instead it becomes ACTIVE and
// opens its ordering window. Orders placed later spawn their own paid
// child bookings that go through the normal pipeline.
const booking = await this.datasource
.getRepository(Booking)
.findOne({ where: { id: input.bookingId } });
const isGeneralContract = booking?.bookingType === "GENERAL_CONTRACT";
let contractExpiresAt: Date | null = null;
if (isGeneralContract) {
const months = await this.contractPeriodMonths();
contractExpiresAt = new Date(paidAt);
contractExpiresAt.setMonth(contractExpiresAt.getMonth() + months);
}
await this.datasource.transaction(async (mg) => {
await mg.update(
PaymentEntity,
{ id: intent.id },
{ status: "success", paidAt, transactionId: input.providerTxnId ?? intent.transactionId },
);
await mg.update(Booking, { id: input.bookingId }, { paymentStatus: "PAID" ,status:"PAID"});
await mg.update(
Booking,
{ id: input.bookingId },
isGeneralContract
? { paymentStatus: "PAID", status: "CONTRACT_ACTIVE", expiresAt: contractExpiresAt }
: { paymentStatus: "PAID", status: "PAID" },
);
});
if (isGeneralContract) {
this.logger.log(
`General contract ${booking?.reference ?? input.bookingId} ACTIVE — ordering open until ${contractExpiresAt?.toISOString()}`,
);
return { alreadyFinalized: false };
}
try {
await this.bookingBatchService.ensurePaidBookingAllocated(input.bookingId);
} catch (err) {
@@ -428,4 +477,8 @@ export class PaymentService {
default: return "action-required";
}
}
async findByCompanyId(companyId: string) {
return this.paymentRepo.findByCompanyId(companyId);
}
}