- payments: cash instant + gateway intent/redirect + webhook confirm; topup credits wallet - payouts: driver request (holds wallet.debit) + admin complete/fail (fail refunds) - tables tripz_pay_payments / tripz_pay_payouts (logical payment schema, extractable later) - providers: cash/cliq/zaincash/syriatel/mtn/shamcash/paymob/fawry (structure ready, creds later) - migration InitPayments; wired into app.module Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { Payout } from './entities/payout.entity';
|
|
import { WalletService } from '../wallet/wallet.service';
|
|
|
|
export interface PayoutRequestDto {
|
|
amount: number;
|
|
channel: string; // bank | cliq | mtn | ecash | syriatel
|
|
currency?: string;
|
|
destination?: Record<string, any>;
|
|
}
|
|
|
|
/**
|
|
* سحب أرباح السائق: يحجز المبلغ من المحفظة فوراً (debit)، ثم يُحوَّل خارجياً.
|
|
* الفشل يُعيد المبلغ للمحفظة. التحويل الخارجي الفعلي يُنفَّذ يدوياً/بمزوّد لاحقاً.
|
|
*/
|
|
@Injectable()
|
|
export class PayoutsService {
|
|
constructor(
|
|
@InjectRepository(Payout) private readonly repo: Repository<Payout>,
|
|
private readonly wallet: WalletService,
|
|
) {}
|
|
|
|
async request(tenantId: string, driverUserId: string, dto: PayoutRequestDto) {
|
|
const amount = Number(dto.amount);
|
|
if (!(amount > 0)) throw new BadRequestException('amount must be > 0');
|
|
if (!dto.channel) throw new BadRequestException('channel is required');
|
|
|
|
// يحجز المبلغ (يرمي لو الرصيد غير كافٍ)
|
|
await this.wallet.debit(tenantId, driverUserId, amount, 'payout_hold');
|
|
|
|
return this.repo.save(
|
|
this.repo.create({
|
|
tenant_id: tenantId,
|
|
driver_user_id: driverUserId,
|
|
amount,
|
|
currency: dto.currency ?? 'JOD',
|
|
channel: dto.channel,
|
|
destination: dto.destination ?? {},
|
|
status: 'requested',
|
|
}),
|
|
);
|
|
}
|
|
|
|
listMine(tenantId: string, driverUserId: string) {
|
|
return this.repo.find({
|
|
where: { tenant_id: tenantId, driver_user_id: driverUserId },
|
|
order: { created_at: 'DESC' },
|
|
});
|
|
}
|
|
|
|
private async getOr404(tenantId: string, id: string): Promise<Payout> {
|
|
const p = await this.repo.findOne({ where: { tenant_id: tenantId, id } });
|
|
if (!p) throw new NotFoundException('payout not found');
|
|
return p;
|
|
}
|
|
|
|
/** الأدمن يؤكّد أن المبلغ حُوِّل خارجياً. */
|
|
async complete(tenantId: string, id: string, ref?: string) {
|
|
const p = await this.getOr404(tenantId, id);
|
|
if (p.status === 'paid') return p;
|
|
p.status = 'paid';
|
|
p.ref = ref ?? p.ref;
|
|
return this.repo.save(p);
|
|
}
|
|
|
|
/** فشل التحويل — يُعاد المبلغ للمحفظة. */
|
|
async fail(tenantId: string, id: string) {
|
|
const p = await this.getOr404(tenantId, id);
|
|
if (p.status === 'paid') throw new BadRequestException('already paid');
|
|
if (p.status !== 'failed') {
|
|
await this.wallet.credit(tenantId, p.driver_user_id, Number(p.amount), 'payout_refund', p.id);
|
|
}
|
|
p.status = 'failed';
|
|
return this.repo.save(p);
|
|
}
|
|
}
|