feat: implement payment result handling; refactor payment components and enhance terminal payment logic

This commit is contained in:
2026-05-17 15:21:06 +03:30
parent 6f1ad20cff
commit 78501b907b
13 changed files with 268 additions and 138 deletions
+2 -3
View File
@@ -1,4 +1,3 @@
# syntax=docker/dockerfile:1.4
# ---------- Build stage ----------
FROM node:20-alpine AS builder
@@ -7,11 +6,11 @@ ARG DIST_DIR=tis
WORKDIR /app
RUN npm config set registry "https://hub.megan.ir/npm/"
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm config set store-dir /pnpm/store && pnpm install --frozen-lockfile
RUN pnpm install --frozen-lockfile
COPY . .
@@ -1,3 +1,4 @@
@defer {
<div class="w-full min-h-[60svh] p-4 md:p-6">
<section class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-surface-200 bg-white">
<div class="bg-gradient-to-l from-emerald-50 via-teal-50 to-cyan-50 p-5 md:p-7">
@@ -6,8 +7,8 @@
</p>
<span class="block mb-4 text-xl font-bold text-surface-900 md:text-3xl">{{ brandingInfo.fullTitle }}</span>
<p class="mt-3 max-w-2xl text-sm leading-7 text-surface-700 md:text-base">
این نسخه برای استفاده روزانه روی دستگاه های PSP بهینه شده است تا عملیات ثبت و صدور صورتحساب‌‌های مالیاتی با سرعت
بالاتر، پایداری بیشتر و تجربه کاربری ساده تر انجام شود.
این نسخه برای استفاده روزانه روی دستگاه های PSP بهینه شده است تا عملیات ثبت و صدور صورتحساب‌‌های مالیاتی با
سرعت بالاتر، پایداری بیشتر و تجربه کاربری ساده تر انجام شود.
</p>
</div>
@@ -59,3 +60,4 @@
</div>
</section>
</div>
}
@@ -45,7 +45,6 @@ export class PosConfigPrintFormComponent extends AbstractForm<
override async ngOnInit() {
this.loading.set(true);
const initialValues = await this.service.get();
console.log('initialValues', initialValues);
this.form.patchValue(initialValues);
this.loading.set(false);
@@ -16,7 +16,7 @@
<div class="form-group">
<uikit-field label="تعداد مراحل پرداخت با پایانه" name="pay_by_terminal_steps">
<p-select
[options]="payByTerminalSteps"
[options]="payByTerminalSteps()"
[ngModel]="selectedPayByTerminalStep()"
[ngModelOptions]="{ standalone: true }"
optionLabel="label"
@@ -33,13 +33,14 @@
[label]="$index === 0 ? 'پرداخت با پایانه' : 'پرداخت با پایانه - مرحله ' + ($index + 1)"
type="price"
[max]="terminalsMax()[$index]"
[disabled]="payByTerminalSteps()[$index].payed"
>
<ng-template #suffixTemp>
<button
pButton
size="small"
type="button"
[disabled]="!remainedAmount()"
[disabled]="!remainedAmount() || payByTerminalSteps()[$index].payed"
(click)="fillTerminalRemained($index)"
>
تمامی بدهی باقی‌مانده
@@ -12,7 +12,13 @@ import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angu
import { ButtonDirective } from 'primeng/button';
import { Select } from 'primeng/select';
import { catchError, throwError } from 'rxjs';
import { IPayment, IPosOrderResponse, TOrderPaymentTypes } from '../../models';
import {
IPayment,
IPosOrderResponse,
PosPaymentResult,
TerminalSuccessPaymentPayload,
TOrderPaymentTypes,
} from '../../models';
import { PosPaymentBridgeAbstract } from '../../services/payment-bridge.abstract';
import { PosPaymentBridgeService } from '../../services/payment-bridge.service';
import { PosLandingStore } from '../../store/main.store';
@@ -48,25 +54,57 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
submitOrderLoading = computed(() => this.store.submitOrderLoading());
payByTerminalSteps = Array.from({ length: 10 }, (_, i) => ({
payByTerminalSteps = signal(
Array.from({ length: 10 }, (_, i) => ({
value: i + 1,
payed: false,
info: {} as TerminalSuccessPaymentPayload,
label: `${i + 1} مرحله‌ای`,
}));
amount: 0,
})),
);
selectedPayByTerminalStep = signal(1);
payedInTerminal = signal<Array<number>>([]);
private restorePaymentCallback: Maybe<() => void> = null;
private readonly injectedPaymentResultHandler = (payload: unknown) => {
alert('پرداخت با موفقیت انجام شد');
this.toastServices.success({ text: 'شد شد شد شد شد' });
// this.handlePaymentResult(payload);
private readonly injectedPaymentResultHandler = (payload: PosPaymentResult) => {
if (payload.status === 'SUCCESS') {
const paidStepId = Number(payload.id || 0);
this.payByTerminalSteps.update((prev) =>
prev.map((step, index) => {
if (step.value === paidStepId) {
return {
...step,
payed: true,
amount: this.terminalControls[step.value - 1]?.value || 0,
info: {
customer_card_no: payload.data?.customerCardNO || '',
description: payload.message || '',
rrn: payload.data?.rrn || '',
stan: payload.data?.stan || '',
terminal_id: payload.data?.terminalId || '',
transaction_date_time: new Date(payload.data?.transactionDateTime || ''),
},
};
}
return step;
}),
);
const terminalControl = this.terminalControls[paidStepId - 1];
if (terminalControl) {
terminalControl.disable({ onlySelf: true, emitEvent: false });
}
const amount = Number(
terminalControl?.value || this.extractAmountFromPaymentResult(payload) || 0,
);
this.toastServices.success({ text: `پرداخت با موفقیت انجام شد. مبلغ: ${amount}` });
} else {
this.toastServices.error({ text: payload.message || 'پرداخت با دستگاه انجام نشد.' });
}
};
ngAfterViewInit() {
console.log('ngOnViewInit');
this.restorePaymentCallback = this.paymentBridge.registerPaymentResultListener(
this.injectedPaymentResultHandler,
);
@@ -118,7 +156,11 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
this.terminalsMax.update((max) => {
const terminalMax = remainedAmount + (terminal.value || 0);
terminal.addValidators([Validators.max(terminalMax)]);
if (terminalMax) {
if (this.isTerminalStepPaid(i + 1)) {
terminal.disable({
onlySelf: true,
});
} else if (terminalMax) {
terminal.enable({
onlySelf: true,
});
@@ -145,7 +187,13 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
}
changePayByTerminalSteps(steps: number) {
const normalizedSteps = Math.max(1, Number(steps) || 1);
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;
@@ -219,10 +267,24 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
}
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: (rawPayment.terminals || []).map((value) => Number(value || 0)),
terminals: terminalPayments,
};
this.store.setPayment(payment);
@@ -230,6 +292,9 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
const terminalPaymentIsDone = this.checkTerminalPayments();
if (terminalPaymentIsDone) {
this.toastService.info({
text: `terminalPayments[0].customer_card_no: ${payment.terminals[0]?.customer_card_no || ''}`,
});
this.store
.submitOrder()
.pipe(
@@ -245,23 +310,15 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
}
private checkTerminalPayments(): boolean {
this.form.controls.terminals.controls = this.form.controls.terminals.controls.filter(
(control) => control.value,
);
const terminalPayments = this.form.controls.terminals.controls.map(
(terminal) => terminal.value!,
);
this.selectedPayByTerminalStep.set(terminalPayments.length || 1);
if (terminalPayments.length) {
const pendingTerminalPayments = this.getPendingTerminalPayments();
if (pendingTerminalPayments.length) {
if (this.nativeBridgeService.isEnabled()) {
for (let i in terminalPayments) {
const terminal = terminalPayments[i];
this.toastServices.info({ text: 'در حال پردازش پرداخت با دستگاه...', life: 3000 });
this.payByTerminal(parseInt(i), terminal);
break;
}
const nextPending = pendingTerminalPayments[0];
this.payByTerminal(nextPending.stepId, nextPending.amount);
this.toastServices.info({
text: 'پس از تایید پرداخت هر مرحله، برای مرحله بعد دوباره روی ثبت پرداخت بزنید.',
life: 3500,
});
} else {
this.toastServices.error({
text: 'امکان ارتباط با دستگاه پرداخت میسر نیست.',
@@ -278,7 +335,7 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
private payByTerminal(id: number, amount: number) {
const payResult = this.nativeBridgeService.pay({
amount,
id: id.toString(),
id: String(id),
});
// if (!payResult.success) {
@@ -288,6 +345,27 @@ export class PosPaymentFormDialogComponent extends AbstractFormDialog<IPayment,
// }
}
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));
}
override onSuccess(response: IPosOrderResponse): void {}
private extractAmountFromPaymentResult(payload: unknown): number | null {
@@ -2,4 +2,5 @@ export * from './customer';
export * from './io';
export * from './payload';
export * from './payment';
export * from './posPaymentResult';
export * from './types';
@@ -1,5 +1,5 @@
export interface IPayment {
terminals: number[];
terminals: IPaymentTerminal[];
cash: number;
set_off: number;
}
@@ -0,0 +1,22 @@
export interface PosPaymentResult {
id?: string;
status: 'SUCCESS' | 'FAILURE';
message?: string;
data?: {
terminalId: string;
stan: string;
rrn: string;
responseCode: string;
customerCardNO: string;
transactionDateTime: string;
};
}
export interface TerminalSuccessPaymentPayload {
terminal_id: string;
stan: string;
rrn: string;
transaction_date_time: Date;
customer_card_no: string;
description: string;
}
@@ -1,4 +1,6 @@
import { PosPaymentResult } from '../models';
export abstract class PosPaymentBridgeAbstract {
abstract registerPaymentResultListener(handler: (payload: unknown) => void): () => void;
abstract registerPaymentResultListener(handler: (payload: PosPaymentResult) => void): () => void;
abstract emitPaymentResultForTest(payload: unknown): void;
}
@@ -1,33 +1,51 @@
import { ToastService } from '@/core/services/toast.service';
import { inject, Injectable } from '@angular/core';
import { PosPaymentResult } from '../models';
import { PosPaymentBridgeAbstract } from './payment-bridge.abstract';
@Injectable({ providedIn: 'root' })
export class PosPaymentBridgeService extends PosPaymentBridgeAbstract {
private readonly toastServices = inject(ToastService);
registerPaymentResultListener(handler: (payload: unknown) => void): () => void {
// const w = window as unknown as {
// WebV?: {
// onPaymentResult?: (payload: unknown) => void;
// AndroidBridge?: {
// onPaymentResult?: (payload: unknown) => void;
// };
// };
// };
this.toastServices.error({ text: '@@@@@@@@@@@@@' });
registerPaymentResultListener(handler: (payload: PosPaymentResult) => void): () => void {
// @ts-ignore
window.WebV = {
onPaymentResult: () => {
this.toastServices.error({ text: 'asdasdsadassadasdas' });
onPaymentResult: (payload: PosPaymentResult) => {
try {
// const parsedPayload: PosPaymentResult = JSON.parse(payload);
// this.toastServices.info({ text: payload.data?.customerCardNO || '', life: 10000 });
// this.toastServices.info({
// text: payload.data?.responseCode || 'No response code',
// life: 10000,
// });
// this.toastServices.info({ text: payload.data?.rrn || 'No response code', life: 10000 });
// this.toastServices.info({ text: payload.data?.stan || 'No response code', life: 10000 });
// this.toastServices.info({
// text: payload.data?.terminalId || 'No response code',
// life: 10000,
// });
// this.toastServices.info({
// text: payload.data?.transactionDateTime || 'No response code',
// life: 10000,
// });
handler(payload);
} catch (e) {
this.toastServices.error({ text: 'Failed to parse payment result', life: 10000 });
this.toastServices.error({
text: e instanceof Error ? e.message : String(e),
life: 10000,
});
this.toastServices.error({ text: typeof payload, life: 10000 });
}
},
};
return () => {};
}
emitPaymentResultForTest(payload: unknown): void {
const w = window as unknown as { WebV?: { onPaymentResult?: (payload: unknown) => void } };
emitPaymentResultForTest(payload: PosPaymentResult): void {
const w = window as unknown as {
WebV?: { onPaymentResult?: (payload: PosPaymentResult) => void };
};
w.WebV?.onPaymentResult?.(payload);
}
}
@@ -1,5 +1,6 @@
import { Maybe } from '@/core';
// import { ICustomerResponse } from '@/modules/customers/models';
import { ToastService } from '@/core/services/toast.service';
import { IGoodCategoryResponse, IGoodResponse } from '@/domains/pos/models/good.io';
import { CustomerType } from '@/shared/localEnum/constants/customerTypes';
import { createUUID, JALALI_DATE_FORMATS, nowJalali } from '@/utils';
@@ -55,6 +56,7 @@ export const INITIAL_POS_STATE: IPosLandingState = {
@Injectable({ providedIn: 'any' })
export class PosLandingStore {
private readonly service = inject(PosService);
private readonly toastServices = inject(ToastService);
private state$ = signal<IPosLandingState>({ ...INITIAL_POS_STATE });
@@ -238,6 +240,10 @@ export class PosLandingStore {
total_amount: this.orderPricingInfo().totalAmount,
};
this.toastServices.info({
text: `payment submitted: ${this.payments().terminals[0]?.customer_card_no || ''}`,
});
let res: Maybe<IPosOrderResponse> = null;
return this.service.submitOrder(orderPayload).pipe(
@@ -1,3 +1,4 @@
@defer {
<div class="w-full min-h-[60svh] p-4 md:p-6">
<section class="mx-auto max-w-3xl overflow-hidden rounded-2xl border border-surface-200 bg-white">
<div class="bg-gradient-to-l from-sky-50 via-cyan-50 to-teal-50 p-5 md:p-7">
@@ -47,3 +48,4 @@
</div>
</section>
</div>
}
+2 -2
View File
@@ -1,8 +1,8 @@
// TIS tenant environment configuration
export const environment = {
production: true,
// apiBaseUrl: 'https://psp-api.shift-am.ir',
apiBaseUrl: 'http://192.168.128.73:5002',
apiBaseUrl: 'https://psp-api.shift-am.ir',
// apiBaseUrl: 'http://192.168.128.73:5002',
host: 'localhost',
port: 5000,
enableLogging: false,