@fiado/type-kit 3.631.0 → 3.632.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_test_/unit/retailIntegration/retailSaleContracts.test.ts +15 -3
- package/_test_/unit/retailWizard/accessorySaleRequests.test.ts +2 -2
- package/_test_/unit/retailWizard/salePayments.test.ts +140 -0
- package/bin/retailIntegration/models/RetailSystemSale.d.ts +1 -1
- package/bin/retailIntegration/models/RetailSystemSale.js +4 -4
- package/bin/retailIntegration/validators/saleValidators.d.ts +1 -1
- package/bin/retailIntegration/validators/saleValidators.js +3 -2
- package/bin/retailWizard/constants/retailWizardValidation.d.ts +5 -0
- package/bin/retailWizard/constants/retailWizardValidation.js +6 -1
- package/bin/retailWizard/dtos/Sale.d.ts +5 -0
- package/bin/retailWizard/dtos/SalePayment.d.ts +11 -0
- package/bin/retailWizard/dtos/SalePayment.js +69 -0
- package/bin/retailWizard/dtos/SalePaymentDetail.d.ts +1 -0
- package/bin/retailWizard/dtos/SalePaymentDetail.js +1 -0
- package/bin/retailWizard/dtos/SellerPendingRefundItem.d.ts +6 -1
- package/bin/retailWizard/dtos/WizardPayment.d.ts +5 -0
- package/bin/retailWizard/dtos/WizardPayment.js +7 -0
- package/bin/retailWizard/dtos/requests/CreateAccessorySaleRequest.d.ts +8 -3
- package/bin/retailWizard/dtos/requests/CreateAccessorySaleRequest.js +10 -2
- package/bin/retailWizard/dtos/requests/DownPaymentRequest.d.ts +7 -2
- package/bin/retailWizard/dtos/requests/DownPaymentRequest.js +9 -1
- package/bin/retailWizard/dtos/requests/RegisterPaymentRequest.d.ts +6 -1
- package/bin/retailWizard/dtos/requests/RegisterPaymentRequest.js +8 -0
- package/bin/retailWizard/events/RetailPromotionInboundMessage.d.ts +3 -1
- package/bin/retailWizard/events/RetailPromotionInboundMessage.js +6 -0
- package/bin/retailWizard/index.d.ts +5 -0
- package/bin/retailWizard/index.js +5 -0
- package/bin/retailWizard/validators/IsSalePaymentList.d.ts +2 -0
- package/bin/retailWizard/validators/IsSalePaymentList.js +22 -0
- package/bin/retailWizard/validators/OnlyForPaymentMethodConstraint.d.ts +6 -0
- package/bin/retailWizard/validators/OnlyForPaymentMethodConstraint.js +25 -0
- package/bin/retailWizard/validators/PaymentsHaveSingleUseMethodsConstraint.d.ts +6 -0
- package/bin/retailWizard/validators/PaymentsHaveSingleUseMethodsConstraint.js +27 -0
- package/bin/retailWizard/validators/PaymentsMatchAmountCentsConstraint.d.ts +6 -0
- package/bin/retailWizard/validators/PaymentsMatchAmountCentsConstraint.js +29 -0
- package/package.json +1 -1
- package/src/retailIntegration/models/RetailSystemSale.ts +4 -4
- package/src/retailIntegration/validators/saleValidators.ts +3 -2
- package/src/retailWizard/constants/retailWizardValidation.ts +8 -0
- package/src/retailWizard/dtos/Sale.ts +5 -0
- package/src/retailWizard/dtos/SalePayment.ts +55 -0
- package/src/retailWizard/dtos/SalePaymentDetail.ts +1 -0
- package/src/retailWizard/dtos/SellerPendingRefundItem.ts +6 -1
- package/src/retailWizard/dtos/WizardPayment.ts +10 -0
- package/src/retailWizard/dtos/requests/CreateAccessorySaleRequest.ts +17 -5
- package/src/retailWizard/dtos/requests/DownPaymentRequest.ts +14 -3
- package/src/retailWizard/dtos/requests/RegisterPaymentRequest.ts +13 -2
- package/src/retailWizard/events/RetailPromotionInboundMessage.ts +7 -1
- package/src/retailWizard/index.ts +5 -0
- package/src/retailWizard/validators/IsSalePaymentList.ts +20 -0
- package/src/retailWizard/validators/OnlyForPaymentMethodConstraint.ts +18 -0
- package/src/retailWizard/validators/PaymentsHaveSingleUseMethodsConstraint.ts +20 -0
- package/src/retailWizard/validators/PaymentsMatchAmountCentsConstraint.ts +21 -0
|
@@ -50,8 +50,10 @@ export declare class RetailPromotionInboundMessage {
|
|
|
50
50
|
lines: RetailPromotionInboundLine[];
|
|
51
51
|
/** Forma de pago; ausente en mensajes viejos. */
|
|
52
52
|
saleType?: SaleTypeEnum;
|
|
53
|
-
/** Medio de cobro, solo en contado. */
|
|
53
|
+
/** @deprecated Usa `paymentMethods`. Medio de cobro, solo en contado. */
|
|
54
54
|
paymentMethod?: SalePaymentMethodEnum | null;
|
|
55
|
+
/** Medios con que se cobró, uno por pago; solo en contado. */
|
|
56
|
+
paymentMethods?: SalePaymentMethodEnum[] | null;
|
|
55
57
|
/** Financiera externa, solo en `EXTERNAL_REFERRAL`. */
|
|
56
58
|
externalProvider?: ExternalProviderEnum | null;
|
|
57
59
|
/** Precio del equipo en centavos; en crédito no es el enganche. */
|
|
@@ -199,6 +199,12 @@ __decorate([
|
|
|
199
199
|
(0, class_validator_1.IsEnum)(SalePaymentMethodEnum_1.SalePaymentMethodEnum),
|
|
200
200
|
__metadata("design:type", String)
|
|
201
201
|
], RetailPromotionInboundMessage.prototype, "paymentMethod", void 0);
|
|
202
|
+
__decorate([
|
|
203
|
+
(0, class_validator_1.IsOptional)(),
|
|
204
|
+
(0, class_validator_1.IsArray)(),
|
|
205
|
+
(0, class_validator_1.IsEnum)(SalePaymentMethodEnum_1.SalePaymentMethodEnum, { each: true }),
|
|
206
|
+
__metadata("design:type", Array)
|
|
207
|
+
], RetailPromotionInboundMessage.prototype, "paymentMethods", void 0);
|
|
202
208
|
__decorate([
|
|
203
209
|
(0, class_validator_1.IsOptional)(),
|
|
204
210
|
(0, class_validator_1.IsEnum)(ExternalProviderEnum_1.ExternalProviderEnum),
|
|
@@ -40,6 +40,11 @@ export * from './dtos/WizardProduct';
|
|
|
40
40
|
export * from './dtos/WizardWelcome';
|
|
41
41
|
export * from './dtos/WizardCreditPromotionBonus';
|
|
42
42
|
export * from './dtos/SalePaymentDetail';
|
|
43
|
+
export * from './dtos/SalePayment';
|
|
44
|
+
export * from './validators/IsSalePaymentList';
|
|
45
|
+
export * from './validators/OnlyForPaymentMethodConstraint';
|
|
46
|
+
export * from './validators/PaymentsHaveSingleUseMethodsConstraint';
|
|
47
|
+
export * from './validators/PaymentsMatchAmountCentsConstraint';
|
|
43
48
|
export * from './dtos/ProspectNewCustomer';
|
|
44
49
|
export * from './dtos/WizardBeneficiary';
|
|
45
50
|
export * from './dtos/WizardLoan';
|
|
@@ -60,6 +60,11 @@ __exportStar(require("./dtos/WizardProduct"), exports);
|
|
|
60
60
|
__exportStar(require("./dtos/WizardWelcome"), exports);
|
|
61
61
|
__exportStar(require("./dtos/WizardCreditPromotionBonus"), exports);
|
|
62
62
|
__exportStar(require("./dtos/SalePaymentDetail"), exports);
|
|
63
|
+
__exportStar(require("./dtos/SalePayment"), exports);
|
|
64
|
+
__exportStar(require("./validators/IsSalePaymentList"), exports);
|
|
65
|
+
__exportStar(require("./validators/OnlyForPaymentMethodConstraint"), exports);
|
|
66
|
+
__exportStar(require("./validators/PaymentsHaveSingleUseMethodsConstraint"), exports);
|
|
67
|
+
__exportStar(require("./validators/PaymentsMatchAmountCentsConstraint"), exports);
|
|
63
68
|
__exportStar(require("./dtos/ProspectNewCustomer"), exports);
|
|
64
69
|
__exportStar(require("./dtos/WizardBeneficiary"), exports);
|
|
65
70
|
__exportStar(require("./dtos/WizardLoan"), exports);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IsSalePaymentList = IsSalePaymentList;
|
|
4
|
+
const class_transformer_1 = require("class-transformer");
|
|
5
|
+
const class_validator_1 = require("class-validator");
|
|
6
|
+
const retailWizardValidation_1 = require("../constants/retailWizardValidation");
|
|
7
|
+
const SalePayment_1 = require("../dtos/SalePayment");
|
|
8
|
+
const PaymentsHaveSingleUseMethodsConstraint_1 = require("./PaymentsHaveSingleUseMethodsConstraint");
|
|
9
|
+
const PaymentsMatchAmountCentsConstraint_1 = require("./PaymentsMatchAmountCentsConstraint");
|
|
10
|
+
/** Reglas de `payments`: 1 a 3 pagos válidos, efectivo y SPEI una vez, y la suma igual a `amountCents`. */
|
|
11
|
+
function IsSalePaymentList() {
|
|
12
|
+
const decorators = [
|
|
13
|
+
(0, class_validator_1.IsArray)(),
|
|
14
|
+
(0, class_validator_1.ArrayMinSize)(1),
|
|
15
|
+
(0, class_validator_1.ArrayMaxSize)(retailWizardValidation_1.MAX_SALE_PAYMENTS),
|
|
16
|
+
(0, class_validator_1.ValidateNested)({ each: true }),
|
|
17
|
+
(0, class_transformer_1.Type)(() => SalePayment_1.SalePayment),
|
|
18
|
+
(0, class_validator_1.Validate)(PaymentsHaveSingleUseMethodsConstraint_1.PaymentsHaveSingleUseMethodsConstraint),
|
|
19
|
+
(0, class_validator_1.Validate)(PaymentsMatchAmountCentsConstraint_1.PaymentsMatchAmountCentsConstraint),
|
|
20
|
+
];
|
|
21
|
+
return (target, propertyKey) => decorators.forEach((decorate) => decorate(target, propertyKey));
|
|
22
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ValidationArguments, type ValidatorConstraintInterface } from 'class-validator';
|
|
2
|
+
/** Un campo de comprobante con valor solo se acepta en el medio de pago al que pertenece. */
|
|
3
|
+
export declare class OnlyForPaymentMethodConstraint implements ValidatorConstraintInterface {
|
|
4
|
+
validate(value: unknown, args: ValidationArguments): boolean;
|
|
5
|
+
defaultMessage(args: ValidationArguments): string;
|
|
6
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.OnlyForPaymentMethodConstraint = void 0;
|
|
10
|
+
const class_validator_1 = require("class-validator");
|
|
11
|
+
/** Un campo de comprobante con valor solo se acepta en el medio de pago al que pertenece. */
|
|
12
|
+
let OnlyForPaymentMethodConstraint = class OnlyForPaymentMethodConstraint {
|
|
13
|
+
validate(value, args) {
|
|
14
|
+
if (value == null)
|
|
15
|
+
return true;
|
|
16
|
+
return args.object.method === args.constraints[0];
|
|
17
|
+
}
|
|
18
|
+
defaultMessage(args) {
|
|
19
|
+
return `${args.property} solo aplica a pagos ${args.constraints[0]}`;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
exports.OnlyForPaymentMethodConstraint = OnlyForPaymentMethodConstraint;
|
|
23
|
+
exports.OnlyForPaymentMethodConstraint = OnlyForPaymentMethodConstraint = __decorate([
|
|
24
|
+
(0, class_validator_1.ValidatorConstraint)({ name: 'onlyForPaymentMethod', async: false })
|
|
25
|
+
], OnlyForPaymentMethodConstraint);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ValidationArguments, type ValidatorConstraintInterface } from 'class-validator';
|
|
2
|
+
/** Efectivo y SPEI aparecen a lo más una vez en `payments`; la tarjeta se puede repetir. */
|
|
3
|
+
export declare class PaymentsHaveSingleUseMethodsConstraint implements ValidatorConstraintInterface {
|
|
4
|
+
validate(value: unknown, _args: ValidationArguments): boolean;
|
|
5
|
+
defaultMessage(): string;
|
|
6
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.PaymentsHaveSingleUseMethodsConstraint = void 0;
|
|
10
|
+
const class_validator_1 = require("class-validator");
|
|
11
|
+
const retailWizardValidation_1 = require("../constants/retailWizardValidation");
|
|
12
|
+
/** Efectivo y SPEI aparecen a lo más una vez en `payments`; la tarjeta se puede repetir. */
|
|
13
|
+
let PaymentsHaveSingleUseMethodsConstraint = class PaymentsHaveSingleUseMethodsConstraint {
|
|
14
|
+
validate(value, _args) {
|
|
15
|
+
if (!Array.isArray(value))
|
|
16
|
+
return true;
|
|
17
|
+
const methods = value.map((p) => p?.method);
|
|
18
|
+
return retailWizardValidation_1.SINGLE_USE_PAYMENT_METHODS.every((m) => methods.filter((x) => x === m).length <= 1);
|
|
19
|
+
}
|
|
20
|
+
defaultMessage() {
|
|
21
|
+
return `payments admite a lo más un pago de cada medio ${retailWizardValidation_1.SINGLE_USE_PAYMENT_METHODS.join(' y ')}`;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
exports.PaymentsHaveSingleUseMethodsConstraint = PaymentsHaveSingleUseMethodsConstraint;
|
|
25
|
+
exports.PaymentsHaveSingleUseMethodsConstraint = PaymentsHaveSingleUseMethodsConstraint = __decorate([
|
|
26
|
+
(0, class_validator_1.ValidatorConstraint)({ name: 'paymentsHaveSingleUseMethods', async: false })
|
|
27
|
+
], PaymentsHaveSingleUseMethodsConstraint);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ValidationArguments, type ValidatorConstraintInterface } from 'class-validator';
|
|
2
|
+
/** La suma de `payments[].amountCents` es el `amountCents` del cobro. */
|
|
3
|
+
export declare class PaymentsMatchAmountCentsConstraint implements ValidatorConstraintInterface {
|
|
4
|
+
validate(value: unknown, args: ValidationArguments): boolean;
|
|
5
|
+
defaultMessage(): string;
|
|
6
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.PaymentsMatchAmountCentsConstraint = void 0;
|
|
10
|
+
const class_validator_1 = require("class-validator");
|
|
11
|
+
/** La suma de `payments[].amountCents` es el `amountCents` del cobro. */
|
|
12
|
+
let PaymentsMatchAmountCentsConstraint = class PaymentsMatchAmountCentsConstraint {
|
|
13
|
+
validate(value, args) {
|
|
14
|
+
const total = args.object.amountCents;
|
|
15
|
+
if (!Array.isArray(value) || !Number.isInteger(total))
|
|
16
|
+
return true;
|
|
17
|
+
const amounts = value.map((p) => p?.amountCents);
|
|
18
|
+
if (!amounts.every((a) => Number.isInteger(a)))
|
|
19
|
+
return true;
|
|
20
|
+
return amounts.reduce((sum, a) => sum + a, 0) === total;
|
|
21
|
+
}
|
|
22
|
+
defaultMessage() {
|
|
23
|
+
return 'la suma de payments[].amountCents debe ser amountCents';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
exports.PaymentsMatchAmountCentsConstraint = PaymentsMatchAmountCentsConstraint;
|
|
27
|
+
exports.PaymentsMatchAmountCentsConstraint = PaymentsMatchAmountCentsConstraint = __decorate([
|
|
28
|
+
(0, class_validator_1.ValidatorConstraint)({ name: 'paymentsMatchAmountCents', async: false })
|
|
29
|
+
], PaymentsMatchAmountCentsConstraint);
|
package/package.json
CHANGED
|
@@ -53,13 +53,13 @@ export class RetailSystemSaleCredit {
|
|
|
53
53
|
@Validate(IsCalendarDate) firstPaymentDate!: string;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/** Un medio de pago de `amountPaid`; las seis llaves van siempre
|
|
56
|
+
/** Un medio de pago de `amountPaid`; las seis llaves van siempre. El comprobante es opcional (`null`) en su medio y `null` en los demás. */
|
|
57
57
|
export class RetailSystemSalePayment {
|
|
58
58
|
@IsIn(Object.values(RetailPaymentMethodEnum)) method!: RetailPaymentMethod;
|
|
59
59
|
@IsString() @Matches(RETAIL_SALE_POSITIVE_DECIMAL_PATTERN) amount!: string;
|
|
60
|
-
@Validate(MatchesForPaymentMethod, [CARD, RETAIL_CARD_LAST4_PATTERN]) cardLast4!: string | null;
|
|
61
|
-
@Validate(MatchesForPaymentMethod, [CARD, RETAIL_SALE_ID_TEXT_PATTERN]) authCode!: string | null;
|
|
62
|
-
@Validate(MatchesForPaymentMethod, [WIRE, RETAIL_SPEI_TRACKING_KEY_PATTERN]) speiTrackingKey!: string | null;
|
|
60
|
+
@Validate(MatchesForPaymentMethod, [CARD, RETAIL_CARD_LAST4_PATTERN, true]) cardLast4!: string | null;
|
|
61
|
+
@Validate(MatchesForPaymentMethod, [CARD, RETAIL_SALE_ID_TEXT_PATTERN, true]) authCode!: string | null;
|
|
62
|
+
@Validate(MatchesForPaymentMethod, [WIRE, RETAIL_SPEI_TRACKING_KEY_PATTERN, true]) speiTrackingKey!: string | null;
|
|
63
63
|
@Validate(MatchesForPaymentMethod, [WIRE, RETAIL_CLABE_PATTERN, true]) senderClabe!: string | null;
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -6,7 +6,7 @@ function decimalToCents(value: string): number {
|
|
|
6
6
|
return Number(value.replace('.', ''));
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/** Campo de un pago que solo aplica a un medio: con ese medio cumple el patrón; con los demás es `null`. */
|
|
9
|
+
/** Campo de un pago que solo aplica a un medio: con ese medio cumple el patrón (o es `null` si es nulable); con los demás es `null`. */
|
|
10
10
|
@ValidatorConstraint({ name: 'matchesForPaymentMethod', async: false })
|
|
11
11
|
export class MatchesForPaymentMethod implements ValidatorConstraintInterface {
|
|
12
12
|
validate(value: unknown, args: ValidationArguments): boolean {
|
|
@@ -17,7 +17,8 @@ export class MatchesForPaymentMethod implements ValidatorConstraintInterface {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
defaultMessage(args: ValidationArguments): string {
|
|
20
|
-
|
|
20
|
+
const nullable = args.constraints[2] === true;
|
|
21
|
+
return `${args.property} debe ser ${nullable ? 'null o válido' : 'válido'} en ${args.constraints[0]} y null en los demás medios`;
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
import { SalePaymentMethodEnum } from '../enums/SalePaymentMethodEnum';
|
|
2
|
+
|
|
1
3
|
/** Nombre de lambda en kebab-case, p. ej. `reconciliation-engine-business`. */
|
|
2
4
|
export const RETAIL_WIZARD_CALLER_SERVICE_PATTERN = /^[a-z0-9-]{1,64}$/;
|
|
5
|
+
|
|
6
|
+
/** Tope de pagos con los que se cobra una venta, un enganche o una venta de mostrador. */
|
|
7
|
+
export const MAX_SALE_PAYMENTS = 3;
|
|
8
|
+
|
|
9
|
+
/** Medios que aparecen a lo más una vez por cobro; la tarjeta se puede repetir. */
|
|
10
|
+
export const SINGLE_USE_PAYMENT_METHODS: readonly SalePaymentMethodEnum[] = [SalePaymentMethodEnum.CASH, SalePaymentMethodEnum.WIRE];
|
|
@@ -7,6 +7,7 @@ import type { CentralSyncStatusEnum } from '../enums/CentralSyncStatusEnum';
|
|
|
7
7
|
import type { CentralCancelStatusEnum } from '../enums/CentralCancelStatusEnum';
|
|
8
8
|
import type { CentralLiveCheckEnum } from '../enums/CentralLiveCheckEnum';
|
|
9
9
|
import type { SalePaymentDetail } from './SalePaymentDetail';
|
|
10
|
+
import type { SalePayment } from './SalePayment';
|
|
10
11
|
import type { SaleLine } from './SaleLine';
|
|
11
12
|
import type { SalePromotionRef } from '../../retailPromotions/dtos/SalePromotionRef';
|
|
12
13
|
import type { SaleCancellation } from './SaleCancellation';
|
|
@@ -57,10 +58,14 @@ export interface Sale {
|
|
|
57
58
|
downPaymentCents: number | null;
|
|
58
59
|
/** Puntero al crédito SOFOM en loan-credit (solo ventas CREDIT; `null` en contado/externa). */
|
|
59
60
|
creditId: string | null;
|
|
61
|
+
/** @deprecated Usa `payments`; con varios pagos lleva el método del primero. */
|
|
60
62
|
paymentMethod: SalePaymentMethodEnum | null;
|
|
61
63
|
/** `LG-T{storeCode}-VTA-{5hex}-{YYYY}W{ww}` (D11) — único vía `UNIQ#FOLIO`. */
|
|
62
64
|
folio: string;
|
|
65
|
+
/** @deprecated Usa el comprobante de cada elemento de `payments`. */
|
|
63
66
|
paymentDetail: SalePaymentDetail | null;
|
|
67
|
+
/** Pagos con que se cobró `amountCents` (1 a 3); `null` sin cobro, ausente en ventas anteriores al campo. */
|
|
68
|
+
payments?: SalePayment[] | null;
|
|
64
69
|
soldAt: string;
|
|
65
70
|
/** Puntero a la entrega de la tarjeta física en `retail-cards-business`. */
|
|
66
71
|
cardDeliveryId: string | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Expose } from 'class-transformer';
|
|
2
|
+
import { IsBoolean, IsEnum, IsInt, Matches, Min, Validate, ValidateIf } from 'class-validator';
|
|
3
|
+
import {
|
|
4
|
+
RETAIL_CARD_LAST4_PATTERN,
|
|
5
|
+
RETAIL_CLABE_PATTERN,
|
|
6
|
+
RETAIL_SALE_ID_TEXT_PATTERN,
|
|
7
|
+
RETAIL_SPEI_TRACKING_KEY_PATTERN,
|
|
8
|
+
} from '../../retailIntegration/constants/retailIntegrationLimits';
|
|
9
|
+
import { SalePaymentMethodEnum } from '../enums/SalePaymentMethodEnum';
|
|
10
|
+
import { OnlyForPaymentMethodConstraint } from '../validators/OnlyForPaymentMethodConstraint';
|
|
11
|
+
|
|
12
|
+
const { CASH, CARD, WIRE } = SalePaymentMethodEnum;
|
|
13
|
+
const hasValue = (field: keyof SalePayment) => (o: SalePayment) => o[field] != null;
|
|
14
|
+
|
|
15
|
+
/** Un pago del cobro. El comprobante es opcional: si viene se valida y solo en su medio; si no, `null` o ausente. */
|
|
16
|
+
export class SalePayment {
|
|
17
|
+
@Expose()
|
|
18
|
+
@IsEnum(SalePaymentMethodEnum)
|
|
19
|
+
method!: SalePaymentMethodEnum;
|
|
20
|
+
|
|
21
|
+
@Expose()
|
|
22
|
+
@IsInt()
|
|
23
|
+
@Min(1)
|
|
24
|
+
amountCents!: number;
|
|
25
|
+
|
|
26
|
+
@Expose()
|
|
27
|
+
@ValidateIf(hasValue('cashConfirmedInRegister'))
|
|
28
|
+
@IsBoolean()
|
|
29
|
+
@Validate(OnlyForPaymentMethodConstraint, [CASH])
|
|
30
|
+
cashConfirmedInRegister?: boolean | null;
|
|
31
|
+
|
|
32
|
+
@Expose()
|
|
33
|
+
@ValidateIf(hasValue('cardLast4'))
|
|
34
|
+
@Matches(RETAIL_CARD_LAST4_PATTERN)
|
|
35
|
+
@Validate(OnlyForPaymentMethodConstraint, [CARD])
|
|
36
|
+
cardLast4?: string | null;
|
|
37
|
+
|
|
38
|
+
@Expose()
|
|
39
|
+
@ValidateIf(hasValue('authCode'))
|
|
40
|
+
@Matches(RETAIL_SALE_ID_TEXT_PATTERN)
|
|
41
|
+
@Validate(OnlyForPaymentMethodConstraint, [CARD])
|
|
42
|
+
authCode?: string | null;
|
|
43
|
+
|
|
44
|
+
@Expose()
|
|
45
|
+
@ValidateIf(hasValue('speiTrackingKey'))
|
|
46
|
+
@Matches(RETAIL_SPEI_TRACKING_KEY_PATTERN)
|
|
47
|
+
@Validate(OnlyForPaymentMethodConstraint, [WIRE])
|
|
48
|
+
speiTrackingKey?: string | null;
|
|
49
|
+
|
|
50
|
+
@Expose()
|
|
51
|
+
@ValidateIf(hasValue('senderClabe'))
|
|
52
|
+
@Matches(RETAIL_CLABE_PATTERN)
|
|
53
|
+
@Validate(OnlyForPaymentMethodConstraint, [WIRE])
|
|
54
|
+
senderClabe?: string | null;
|
|
55
|
+
}
|
|
@@ -5,6 +5,7 @@ import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
|
|
5
5
|
* Detalle del pago según el medio (`Sale.paymentDetail`): efectivo `{cashConfirmedInRegister}` ·
|
|
6
6
|
* tarjeta `{cardLast4, authCode}` · SPEI `{speiTrackingKey, senderClabe?}`. SureKeep Fase 2 — pista Retail.
|
|
7
7
|
*/
|
|
8
|
+
/** @deprecated Usa `SalePayment`, un elemento por pago. */
|
|
8
9
|
export class SalePaymentDetail {
|
|
9
10
|
@Expose()
|
|
10
11
|
@IsOptional()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RefundStatusEnum } from '../enums/RefundStatusEnum';
|
|
2
2
|
import { SalePaymentMethodEnum } from '../enums/SalePaymentMethodEnum';
|
|
3
|
+
import type { SalePayment } from './SalePayment';
|
|
3
4
|
|
|
4
5
|
/** Un reembolso de enganche de la tienda en el Portal Vendedor (GET /portal/pending-refunds). */
|
|
5
6
|
export interface SellerPendingRefundItem {
|
|
@@ -10,8 +11,12 @@ export interface SellerPendingRefundItem {
|
|
|
10
11
|
creditId: string | null;
|
|
11
12
|
folio: string | null;
|
|
12
13
|
refundPendingCents: number;
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated Usa `payments`. `null` = el registro no guardó con qué se cobró el enganche; no se puede marcar entregado.
|
|
16
|
+
*/
|
|
14
17
|
paymentMethod: SalePaymentMethodEnum | null;
|
|
18
|
+
/** Pagos con que se cobró el enganche; ausente en registros anteriores al campo. */
|
|
19
|
+
payments?: SalePayment[] | null;
|
|
15
20
|
status: RefundStatusEnum;
|
|
16
21
|
/** `true` si el vendedor puede marcarlo entregado: efectivo y pendiente. */
|
|
17
22
|
deliverable: boolean;
|
|
@@ -2,9 +2,12 @@ import { Expose } from 'class-transformer';
|
|
|
2
2
|
import { IsEnum, IsInt, IsOptional, IsString, Matches, Min } from 'class-validator';
|
|
3
3
|
import { regexIso8601 } from '../../helpers/constans/regex';
|
|
4
4
|
import { SalePaymentMethodEnum } from '../enums/SalePaymentMethodEnum';
|
|
5
|
+
import { IsSalePaymentList } from '../validators/IsSalePaymentList';
|
|
6
|
+
import { SalePayment } from './SalePayment';
|
|
5
7
|
|
|
6
8
|
/** Registro del pago de contado dentro de la sesión (`WizardSession.payment`). SureKeep Fase 2 — pista Retail. */
|
|
7
9
|
export class WizardPayment {
|
|
10
|
+
/** @deprecated Usa `payments`; con varios pagos lleva el método del primero. */
|
|
8
11
|
@Expose()
|
|
9
12
|
@IsEnum(SalePaymentMethodEnum)
|
|
10
13
|
method!: SalePaymentMethodEnum;
|
|
@@ -14,11 +17,18 @@ export class WizardPayment {
|
|
|
14
17
|
@Min(0)
|
|
15
18
|
amountCents!: number;
|
|
16
19
|
|
|
20
|
+
/** @deprecated Usa `payments`; es el comprobante como texto JSON. */
|
|
17
21
|
@Expose()
|
|
18
22
|
@IsOptional()
|
|
19
23
|
@IsString()
|
|
20
24
|
detail?: string;
|
|
21
25
|
|
|
26
|
+
/** Pagos del cobro (1 a 3); ausente en sesiones anteriores al campo. */
|
|
27
|
+
@Expose()
|
|
28
|
+
@IsOptional()
|
|
29
|
+
@IsSalePaymentList()
|
|
30
|
+
payments?: SalePayment[];
|
|
31
|
+
|
|
22
32
|
@Expose()
|
|
23
33
|
@IsString()
|
|
24
34
|
@Matches(regexIso8601, { message: 'paidAt debe ser una fecha ISO 8601' })
|
|
@@ -3,19 +3,22 @@ import {
|
|
|
3
3
|
ArrayMaxSize,
|
|
4
4
|
ArrayMinSize,
|
|
5
5
|
IsArray,
|
|
6
|
-
IsDefined,
|
|
7
6
|
IsEnum,
|
|
8
7
|
IsInt,
|
|
8
|
+
IsOptional,
|
|
9
9
|
Min,
|
|
10
|
+
ValidateIf,
|
|
10
11
|
ValidateNested,
|
|
11
12
|
} from 'class-validator';
|
|
12
13
|
import { SalePaymentMethodEnum } from '../../enums/SalePaymentMethodEnum';
|
|
13
14
|
import { SalePaymentDetail } from '../SalePaymentDetail';
|
|
14
15
|
import { AccessorySaleLine } from '../AccessorySaleLine';
|
|
16
|
+
import { SalePayment } from '../SalePayment';
|
|
17
|
+
import { IsSalePaymentList } from '../../validators/IsSalePaymentList';
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* POST /wizard/accessory-sales — cierra la venta de mostrador, sin cliente.
|
|
18
|
-
* `amountCents` es lo cobrado en caja
|
|
21
|
+
* `amountCents` es lo cobrado en caja: iguala la suma del catálogo y la de `payments`.
|
|
19
22
|
*/
|
|
20
23
|
export class CreateAccessorySaleRequest {
|
|
21
24
|
@Expose()
|
|
@@ -26,18 +29,27 @@ export class CreateAccessorySaleRequest {
|
|
|
26
29
|
@Type(() => AccessorySaleLine)
|
|
27
30
|
lines!: AccessorySaleLine[];
|
|
28
31
|
|
|
32
|
+
/** @deprecated Usa `payments`; obligatorio solo si `payments` no viene. */
|
|
29
33
|
@Expose()
|
|
34
|
+
@ValidateIf((o: { payments?: unknown; paymentMethod?: unknown }) => o.payments == null || o.paymentMethod !== undefined)
|
|
30
35
|
@IsEnum(SalePaymentMethodEnum)
|
|
31
|
-
paymentMethod
|
|
36
|
+
paymentMethod?: SalePaymentMethodEnum;
|
|
32
37
|
|
|
33
38
|
@Expose()
|
|
34
39
|
@IsInt()
|
|
35
40
|
@Min(0)
|
|
36
41
|
amountCents!: number;
|
|
37
42
|
|
|
43
|
+
/** @deprecated Usa el comprobante de cada elemento de `payments`. */
|
|
38
44
|
@Expose()
|
|
39
|
-
@
|
|
45
|
+
@IsOptional()
|
|
40
46
|
@ValidateNested()
|
|
41
47
|
@Type(() => SalePaymentDetail)
|
|
42
|
-
paymentDetail
|
|
48
|
+
paymentDetail?: SalePaymentDetail;
|
|
49
|
+
|
|
50
|
+
/** Pagos del cobro (1 a 3); si viene, manda sobre `paymentMethod` y `paymentDetail`. */
|
|
51
|
+
@Expose()
|
|
52
|
+
@IsOptional()
|
|
53
|
+
@IsSalePaymentList()
|
|
54
|
+
payments?: SalePayment[];
|
|
43
55
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Expose, Type } from 'class-transformer';
|
|
2
|
-
import { IsEnum, IsInt, IsOptional, Min, ValidateNested } from 'class-validator';
|
|
2
|
+
import { IsEnum, IsInt, IsOptional, Min, ValidateIf, ValidateNested } from 'class-validator';
|
|
3
3
|
import { SalePaymentMethodEnum } from '../../enums/SalePaymentMethodEnum';
|
|
4
4
|
import { SalePaymentDetail } from '../SalePaymentDetail';
|
|
5
|
+
import { SalePayment } from '../SalePayment';
|
|
6
|
+
import { IsSalePaymentList } from '../../validators/IsSalePaymentList';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* POST /wizard/sessions/:id/down-payment — cobro del ENGANCHE de la venta a crédito (paso 07,
|
|
@@ -9,22 +11,31 @@ import { SalePaymentDetail } from '../SalePaymentDetail';
|
|
|
9
11
|
* `amountCents == downPaymentCents` del crédito (`loan-credit GET /credits/{id}`), NO el precio
|
|
10
12
|
* full. Idempotente por sesión. El folio `SK-…-ENG-…` lo genera el server (no viaja en el body).
|
|
11
13
|
* Al confirmar, el wizard marca la condición 1 del checklist (`activate-check { downPayment }`).
|
|
12
|
-
*
|
|
14
|
+
* El comprobante de tarjeta y SPEI es opcional; `amountCents` es la suma de `payments`.
|
|
13
15
|
* SureKeep Fase 2 — pista SOFOM.
|
|
14
16
|
*/
|
|
15
17
|
export class DownPaymentRequest {
|
|
18
|
+
/** @deprecated Usa `payments`; obligatorio solo si `payments` no viene. */
|
|
16
19
|
@Expose()
|
|
20
|
+
@ValidateIf((o: { payments?: unknown; method?: unknown }) => o.payments == null || o.method !== undefined)
|
|
17
21
|
@IsEnum(SalePaymentMethodEnum)
|
|
18
|
-
method
|
|
22
|
+
method?: SalePaymentMethodEnum;
|
|
19
23
|
|
|
20
24
|
@Expose()
|
|
21
25
|
@IsInt()
|
|
22
26
|
@Min(0)
|
|
23
27
|
amountCents!: number;
|
|
24
28
|
|
|
29
|
+
/** @deprecated Usa el comprobante de cada elemento de `payments`. */
|
|
25
30
|
@Expose()
|
|
26
31
|
@IsOptional()
|
|
27
32
|
@Type(() => SalePaymentDetail)
|
|
28
33
|
@ValidateNested()
|
|
29
34
|
detail?: SalePaymentDetail;
|
|
35
|
+
|
|
36
|
+
/** Pagos del cobro (1 a 3); si viene, manda sobre `method` y `detail`. */
|
|
37
|
+
@Expose()
|
|
38
|
+
@IsOptional()
|
|
39
|
+
@IsSalePaymentList()
|
|
40
|
+
payments?: SalePayment[];
|
|
30
41
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Expose, Type } from 'class-transformer';
|
|
2
|
-
import { IsEnum, IsInt, IsOptional, ValidateNested, Min } from 'class-validator';
|
|
2
|
+
import { IsEnum, IsInt, IsOptional, ValidateIf, ValidateNested, Min } from 'class-validator';
|
|
3
3
|
import { SalePaymentMethodEnum } from '../../enums/SalePaymentMethodEnum';
|
|
4
4
|
import { SalePaymentDetail } from '../SalePaymentDetail';
|
|
5
|
+
import { SalePayment } from '../SalePayment';
|
|
6
|
+
import { IsSalePaymentList } from '../../validators/IsSalePaymentList';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* POST /wizard/sessions/:id/payment — registra el cobro de contado (idempotente por sesión). Regla
|
|
@@ -9,18 +11,27 @@ import { SalePaymentDetail } from '../SalePaymentDetail';
|
|
|
9
11
|
* (D11) ANTES del registro; el folio no viaja en el body (lo posee el server). SureKeep Fase 2 — pista Retail.
|
|
10
12
|
*/
|
|
11
13
|
export class RegisterPaymentRequest {
|
|
14
|
+
/** @deprecated Usa `payments`; obligatorio solo si `payments` no viene. */
|
|
12
15
|
@Expose()
|
|
16
|
+
@ValidateIf((o: { payments?: unknown; method?: unknown }) => o.payments == null || o.method !== undefined)
|
|
13
17
|
@IsEnum(SalePaymentMethodEnum)
|
|
14
|
-
method
|
|
18
|
+
method?: SalePaymentMethodEnum;
|
|
15
19
|
|
|
16
20
|
@Expose()
|
|
17
21
|
@IsInt()
|
|
18
22
|
@Min(0)
|
|
19
23
|
amountCents!: number;
|
|
20
24
|
|
|
25
|
+
/** @deprecated Usa el comprobante de cada elemento de `payments`. */
|
|
21
26
|
@Expose()
|
|
22
27
|
@IsOptional()
|
|
23
28
|
@Type(() => SalePaymentDetail)
|
|
24
29
|
@ValidateNested()
|
|
25
30
|
detail?: SalePaymentDetail;
|
|
31
|
+
|
|
32
|
+
/** Pagos del cobro (1 a 3); si viene, manda sobre `method` y `detail`. */
|
|
33
|
+
@Expose()
|
|
34
|
+
@IsOptional()
|
|
35
|
+
@IsSalePaymentList()
|
|
36
|
+
payments?: SalePayment[];
|
|
26
37
|
}
|
|
@@ -174,11 +174,17 @@ export class RetailPromotionInboundMessage {
|
|
|
174
174
|
@IsEnum(SaleTypeEnum)
|
|
175
175
|
saleType?: SaleTypeEnum;
|
|
176
176
|
|
|
177
|
-
/** Medio de cobro, solo en contado. */
|
|
177
|
+
/** @deprecated Usa `paymentMethods`. Medio de cobro, solo en contado. */
|
|
178
178
|
@IsOptional()
|
|
179
179
|
@IsEnum(SalePaymentMethodEnum)
|
|
180
180
|
paymentMethod?: SalePaymentMethodEnum | null;
|
|
181
181
|
|
|
182
|
+
/** Medios con que se cobró, uno por pago; solo en contado. */
|
|
183
|
+
@IsOptional()
|
|
184
|
+
@IsArray()
|
|
185
|
+
@IsEnum(SalePaymentMethodEnum, { each: true })
|
|
186
|
+
paymentMethods?: SalePaymentMethodEnum[] | null;
|
|
187
|
+
|
|
182
188
|
/** Financiera externa, solo en `EXTERNAL_REFERRAL`. */
|
|
183
189
|
@IsOptional()
|
|
184
190
|
@IsEnum(ExternalProviderEnum)
|
|
@@ -45,6 +45,11 @@ export * from './dtos/WizardProduct';
|
|
|
45
45
|
export * from './dtos/WizardWelcome';
|
|
46
46
|
export * from './dtos/WizardCreditPromotionBonus';
|
|
47
47
|
export * from './dtos/SalePaymentDetail';
|
|
48
|
+
export * from './dtos/SalePayment';
|
|
49
|
+
export * from './validators/IsSalePaymentList';
|
|
50
|
+
export * from './validators/OnlyForPaymentMethodConstraint';
|
|
51
|
+
export * from './validators/PaymentsHaveSingleUseMethodsConstraint';
|
|
52
|
+
export * from './validators/PaymentsMatchAmountCentsConstraint';
|
|
48
53
|
export * from './dtos/ProspectNewCustomer';
|
|
49
54
|
export * from './dtos/WizardBeneficiary';
|
|
50
55
|
export * from './dtos/WizardLoan';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Type } from 'class-transformer';
|
|
2
|
+
import { ArrayMaxSize, ArrayMinSize, IsArray, Validate, ValidateNested } from 'class-validator';
|
|
3
|
+
import { MAX_SALE_PAYMENTS } from '../constants/retailWizardValidation';
|
|
4
|
+
import { SalePayment } from '../dtos/SalePayment';
|
|
5
|
+
import { PaymentsHaveSingleUseMethodsConstraint } from './PaymentsHaveSingleUseMethodsConstraint';
|
|
6
|
+
import { PaymentsMatchAmountCentsConstraint } from './PaymentsMatchAmountCentsConstraint';
|
|
7
|
+
|
|
8
|
+
/** Reglas de `payments`: 1 a 3 pagos válidos, efectivo y SPEI una vez, y la suma igual a `amountCents`. */
|
|
9
|
+
export function IsSalePaymentList(): PropertyDecorator {
|
|
10
|
+
const decorators: PropertyDecorator[] = [
|
|
11
|
+
IsArray(),
|
|
12
|
+
ArrayMinSize(1),
|
|
13
|
+
ArrayMaxSize(MAX_SALE_PAYMENTS),
|
|
14
|
+
ValidateNested({ each: true }),
|
|
15
|
+
Type(() => SalePayment),
|
|
16
|
+
Validate(PaymentsHaveSingleUseMethodsConstraint),
|
|
17
|
+
Validate(PaymentsMatchAmountCentsConstraint),
|
|
18
|
+
];
|
|
19
|
+
return (target, propertyKey) => decorators.forEach((decorate) => decorate(target, propertyKey));
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ValidatorConstraint,
|
|
3
|
+
type ValidationArguments,
|
|
4
|
+
type ValidatorConstraintInterface,
|
|
5
|
+
} from 'class-validator';
|
|
6
|
+
|
|
7
|
+
/** Un campo de comprobante con valor solo se acepta en el medio de pago al que pertenece. */
|
|
8
|
+
@ValidatorConstraint({ name: 'onlyForPaymentMethod', async: false })
|
|
9
|
+
export class OnlyForPaymentMethodConstraint implements ValidatorConstraintInterface {
|
|
10
|
+
validate(value: unknown, args: ValidationArguments): boolean {
|
|
11
|
+
if (value == null) return true;
|
|
12
|
+
return (args.object as { method?: unknown }).method === args.constraints[0];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
defaultMessage(args: ValidationArguments): string {
|
|
16
|
+
return `${args.property} solo aplica a pagos ${args.constraints[0]}`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ValidatorConstraint,
|
|
3
|
+
type ValidationArguments,
|
|
4
|
+
type ValidatorConstraintInterface,
|
|
5
|
+
} from 'class-validator';
|
|
6
|
+
import { SINGLE_USE_PAYMENT_METHODS } from '../constants/retailWizardValidation';
|
|
7
|
+
|
|
8
|
+
/** Efectivo y SPEI aparecen a lo más una vez en `payments`; la tarjeta se puede repetir. */
|
|
9
|
+
@ValidatorConstraint({ name: 'paymentsHaveSingleUseMethods', async: false })
|
|
10
|
+
export class PaymentsHaveSingleUseMethodsConstraint implements ValidatorConstraintInterface {
|
|
11
|
+
validate(value: unknown, _args: ValidationArguments): boolean {
|
|
12
|
+
if (!Array.isArray(value)) return true;
|
|
13
|
+
const methods = value.map((p) => (p as { method?: unknown } | null)?.method);
|
|
14
|
+
return SINGLE_USE_PAYMENT_METHODS.every((m) => methods.filter((x) => x === m).length <= 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
defaultMessage(): string {
|
|
18
|
+
return `payments admite a lo más un pago de cada medio ${SINGLE_USE_PAYMENT_METHODS.join(' y ')}`;
|
|
19
|
+
}
|
|
20
|
+
}
|