d6aa165592
- Refactor PosPaymentBridgeAbstract to enforce PosPaymentResult type for emitPaymentResultForTest method. - Simplify PosPaymentBridgeService by removing commented-out code and improving error handling. - Replace PosPaymentFormDialogComponent with SharedInvoicePaymentFormDialog in root.component.html for better payment handling. - Enhance root.component.ts to manage payment form visibility and submission logic. - Update light-bottomsheet.component.html for improved styling. - Add new return form features including item removal and total price calculation in returnForm components. - Introduce new payment form dialog for handling invoice payments with detailed structure and validation. - Implement payment handling logic in sale-invoice-single-view component to support correction payments. - Ensure proper integration of payment components in the shared components index.
451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
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<IPayment, any> {
|
|
@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<number | null>[];
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|