import { Maybe } from '@/core'; import { NativeBridgeService } from '@/core/services'; import { ToastService } from '@/core/services/toast.service'; import { IPayment, PosPaymentResult, TerminalSuccessPaymentPayload, TOrderPaymentTypes, } from '@/domains/pos/modules/shop/models'; import { PosPaymentBridgeAbstract } from '@/domains/pos/modules/shop/services/payment-bridge.abstract'; import { PosPaymentBridgeService } from '@/domains/pos/modules/shop/services/payment-bridge.service'; import { AbstractFormDialog } from '@/shared/abstractClasses'; import { KeyValueComponent } from '@/shared/components'; import { SharedLightBottomsheetComponent } from '@/shared/components/dialog/light-bottomsheet.component'; import { FormFooterActionsComponent } from '@/shared/components/formFooterActions/form-footer-actions.component'; import { PriceInputComponent } from '@/shared/components/input/price-input.component'; import { UikitFieldComponent, UikitLabelComponent } from '@/uikit'; import { formatWithCurrency } from '@/utils'; import { Component, EventEmitter, inject, Input, Output, signal, SimpleChanges, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { ButtonDirective } from 'primeng/button'; import { Select } from 'primeng/select'; import { SelectButton } from 'primeng/selectbutton'; @Component({ selector: 'shared-invoice-payment-form-dialog', templateUrl: './form-dialog.component.html', imports: [ FormsModule, ReactiveFormsModule, FormFooterActionsComponent, KeyValueComponent, ButtonDirective, SharedLightBottomsheetComponent, Select, UikitFieldComponent, SelectButton, UikitLabelComponent, PriceInputComponent, ], providers: [{ provide: PosPaymentBridgeAbstract, useExisting: PosPaymentBridgeService }], }) export class PosPaymentFormDialogComponent extends AbstractFormDialog { @Input() totalAmount = 0; @Input() submitOrderLoading = false; @Input() isGoldGuild = false; @Input() isUnknownCustomer = false; @Output() onSubmitPayment = new EventEmitter<{ payment: IPayment; settlementType: 'CASH' | 'CREDIT' | 'MIXED'; }>(); private readonly paymentBridge = inject(PosPaymentBridgeAbstract); private readonly nativeBridgeService = inject(NativeBridgeService); private readonly toastServices = inject(ToastService); readonly settlementTypeItems = [ { label: 'نقدی', value: 'CASH', }, { label: 'نسیه', value: 'CREDIT', }, { label: 'نقدی / نسیه', value: 'MIXED', }, ]; readonly selectedSettlementType = signal<'CASH' | 'CREDIT' | 'MIXED'>('CASH'); readonly remainedAmount = signal(0); setOffMax = signal(this.remainedAmount()); cashMax = signal(this.remainedAmount()); terminalsMax = signal([this.remainedAmount()]); payByTerminalSteps = signal( Array.from({ length: 10 }, (_, i) => ({ value: i + 1, payed: false, info: {} as TerminalSuccessPaymentPayload, label: `${i + 1} مرحله‌ای`, amount: 0, })) ); selectedPayByTerminalStep = signal(1); private restorePaymentCallback: Maybe<() => void> = null; private readonly injectedPaymentResultHandler = (payload: PosPaymentResult) => { if (payload.status === 'SUCCESS') { const paidStepId = this.resolvePaidStepId(payload); const terminalId = payload.data?.terminalId || ''; const stan = payload.data?.stan || ''; const rrn = payload.data?.rrn || ''; const customerCardNo = payload.data?.customerCardNO || ''; const transactionDateTime = payload.data?.transactionDateTime; const targetIndex = Math.max(0, paidStepId - 1); const prevSteps = this.payByTerminalSteps(); const steps = [...prevSteps]; const currentStep = steps[targetIndex]; if (!currentStep) return; steps[targetIndex] = { ...currentStep, payed: true, amount: Number(this.terminalControls[targetIndex]?.value || currentStep.amount || 0), info: { customer_card_no: customerCardNo, description: payload.message || '', rrn, stan, terminal_id: terminalId, transaction_date_time: transactionDateTime ? new Date(transactionDateTime) : new Date(), }, }; this.payByTerminalSteps.set(steps); const terminalControl = this.terminalControls[targetIndex]; if (terminalControl) { terminalControl.disable({ onlySelf: true, emitEvent: false }); } const amount = Number( terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0 ); let successMessage = ''; if (this.payByTerminalSteps()?.length > 1) { successMessage = `پرداخت مرحله ${paidStepId} به مبلغ ${formatWithCurrency(amount)} با موفقیت انجام شد.`; } else { successMessage = `پرداخت به مبلغ ${formatWithCurrency(amount)} با موفقیت انجام شد.`; } this.toastServices.success({ text: successMessage, }); } else { this.toastServices.error({ text: payload.message || 'پرداخت با دستگاه انجام نشد.' }); } }; ngAfterViewInit() { this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener( this.injectedPaymentResultHandler ); } private initForm = () => { const form = this.fb.group({ set_off: [0], cash: [0], terminals: this.fb.array([this.fb.control(0)]), }); form.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { const terminalTotal = (value.terminals || []).reduce((acc, curr) => (acc || 0) + (curr || 0), 0) || 0; const totalValue = (value.cash || 0) + (value.set_off || 0) + terminalTotal; const remainedAmount = this.totalAmount - totalValue; const setOffMax = remainedAmount + (value.set_off || 0); const cashMax = remainedAmount + (value.cash || 0); form.controls.set_off.clearValidators(); form.controls.set_off.addValidators([Validators.max(setOffMax)]); if (!setOffMax) { form.controls.set_off.disable({ onlySelf: true, }); } else { form.controls.set_off.enable({ onlySelf: true, }); } form.controls.cash.clearValidators(); form.controls.cash.addValidators([Validators.max(cashMax)]); if (!cashMax) { form.controls.cash.disable({ onlySelf: true, }); } else { form.controls.cash.enable({ onlySelf: true, }); } form.controls.terminals.controls.forEach((terminal, i) => { terminal.clearValidators(); this.terminalsMax.update((max) => { const terminalMax = remainedAmount + (terminal.value || 0); terminal.addValidators([Validators.max(terminalMax)]); if (this.isTerminalStepPaid(i + 1)) { terminal.disable({ onlySelf: true, }); } else if (terminalMax) { terminal.enable({ onlySelf: true, }); } else { terminal.disable({ onlySelf: true, }); } return max.map((m, index) => (index === i ? terminalMax : m)); }); }); this.remainedAmount.set(remainedAmount); this.cashMax.set(cashMax); this.setOffMax.set(setOffMax); }); return form; }; form = this.initForm(); get terminalControls() { return this.form.controls.terminals.controls as FormControl[]; } changePayByTerminalSteps(steps: number) { const minSteps = this.getMinAllowedSteps(); const normalizedSteps = Math.max(minSteps, Number(steps) || 1); if ((Number(steps) || 1) < minSteps) { this.toastServices.warn({ text: `حداقل تعداد مراحل به دلیل پرداخت های انجام شده ${minSteps} است.`, }); } this.selectedPayByTerminalStep.set(normalizedSteps); const terminals = this.form.controls.terminals; const currentLength = terminals.length; if (normalizedSteps > currentLength) { for (let i = currentLength; i < normalizedSteps; i += 1) { terminals.push(this.fb.control(0)); const terminalMax = this.remainedAmount(); this.form.controls.terminals.controls[i].addValidators([Validators.max(terminalMax)]); this.terminalsMax.update((max) => [...max, terminalMax]); } return; } if (normalizedSteps < currentLength) { const removedSum = terminals.controls .slice(normalizedSteps) .reduce((acc, control) => acc + (control.value || 0), 0); this.remainedAmount.update((value) => value + removedSum); while (terminals.length > normalizedSteps) { terminals.removeAt(terminals.length - 1); } // terminals.at(0)?.setValue(firstValue + removedSum); } } fillTerminalRemained(index: number) { this.fillRemained('TERMINAL', index); } fillRemained(type: TOrderPaymentTypes, _terminalIndex?: number) { switch (type) { case 'TERMINAL': const terminalIndex = _terminalIndex ?? 0; this.form.controls.terminals .at(terminalIndex) ?.setValue( (this.form.controls.terminals.at(terminalIndex)?.value || 0) + this.remainedAmount() ); break; case 'CASH': this.form.controls.cash.setValue( (this.form.controls.cash.value || 0) + this.remainedAmount() ); break; case 'SET_OFF': this.form.controls.set_off.setValue( (this.form.controls.set_off.value || 0) + this.remainedAmount() ); break; } } override showSuccessMessage = false; private resetLimits() { const amount = Number(this.totalAmount || 0); this.remainedAmount.set(amount); this.cashMax.set(amount); this.setOffMax.set(amount); this.terminalsMax.set(this.terminalControls.map(() => amount)); } override ngOnInit(): void { super.ngOnInit(); this.resetLimits(); } override ngOnChanges(changes: SimpleChanges): void { super.ngOnChanges(changes); if (changes['totalAmount']) { this.resetLimits(); } } ngOnDestroy() { this.restorePaymentCallback?.(); } override submitForm() { if (this.selectedSettlementType() === 'CASH') { if (this.remainedAmount() > 0) { return this.toastServices.warn({ text: 'صورت‌حساب‌ تسویه نشده است. لطفا مبلغ پرداخت را کامل کنید..', }); } } const rawPayment = this.form.getRawValue(); const terminalPayments = this.payByTerminalSteps() .slice(0, this.selectedPayByTerminalStep()) .filter((step) => step.payed) .map((step) => ({ amount: Number(this.terminalControls[step.value - 1]?.value || step.amount || 0), terminal_id: step.info.terminal_id || '', stan: step.info.stan || '', rrn: step.info.rrn || '', response_code: '00', customer_card_no: step.info.customer_card_no || '', transaction_date_time: step.info.transaction_date_time || new Date(), description: step.info.description || '', })); const payment: IPayment = { cash: Number(rawPayment.cash || 0), set_off: Number(rawPayment.set_off || 0), terminals: terminalPayments, }; const terminalPaymentIsDone = this.checkTerminalPayments(); if (terminalPaymentIsDone) { this.onSubmitPayment.emit({ payment, settlementType: this.selectedSettlementType(), }); } } private checkTerminalPayments(): boolean { const pendingTerminalPayments = this.getPendingTerminalPayments(); if (pendingTerminalPayments.length) { // if (this.nativeBridgeService.isEnabled()) { const nextPending = pendingTerminalPayments[0]; this.payByTerminal(nextPending.stepId, nextPending.amount); this.toastServices.info({ text: 'پس از تایید پرداخت هر مرحله، برای مرحله بعد دوباره روی ثبت پرداخت بزنید.', life: 3500, }); // } else { // this.toastServices.error({ // text: 'امکان ارتباط با دستگاه پرداخت میسر نیست.', // }); // return false; // } return false; } return true; } private payByTerminal(id: number, amount: number) { // this.paymentBridge.emitPaymentResultForTest({ // status: 'SUCCESS', // id: id.toString(), // data: { // customerCardNO: '123', // responseCode: '2', // rrn: 'rrn', // stan: 'stan', // terminalId: 'terminalId', // transactionDateTime: new Date().toISOString(), // }, // }); const payResult = this.nativeBridgeService.pay({ amount, id: String(id), }); } private isTerminalStepPaid(stepId: number): boolean { return this.payByTerminalSteps().some((step) => step.value === stepId && step.payed); } private getMinAllowedSteps() { const paidSteps = this.payByTerminalSteps() .slice(0, this.selectedPayByTerminalStep()) .filter((step) => step.payed) .map((step) => step.value); return paidSteps.length ? Math.max(...paidSteps) : 1; } private getPendingTerminalPayments() { return this.terminalControls .map((control, index) => ({ stepId: index + 1, amount: Number(control.value || 0), })) .filter(({ stepId, amount }) => amount > 0 && !this.isTerminalStepPaid(stepId)); } private extractAmountFromPaymentResult(payload: unknown): number | null { if (!payload || typeof payload !== 'object') return null; const amount = (payload as { amount?: unknown }).amount; if (typeof amount === 'number' && Number.isFinite(amount)) { return amount; } return null; } private resolvePaidStepId(payload: PosPaymentResult): number { const rawId = Number(payload.id || 0); if (rawId > 0 && this.payByTerminalSteps().some((step) => step.value === rawId)) return rawId; const pendingStep = this.terminalControls.findIndex( (control, index) => Number(control.value || 0) > 0 && !this.isTerminalStepPaid(index + 1) ); return pendingStep >= 0 ? pendingStep + 1 : 1; } }