cardus 0.0.103 → 0.0.106
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/__tests__/insertTempShipments.test.ts +1370 -0
- package/dist/__tests__/insertTempShipments.test.js +1150 -0
- package/dist/index.js +109 -105
- package/dist/jest.config.js +18 -0
- package/index.ts +147 -157
- package/jest.config.js +17 -0
- package/package.json +3 -1
- package/types/index.ts +1 -1
package/index.ts
CHANGED
|
@@ -17,21 +17,18 @@ import {
|
|
|
17
17
|
LlmAPIService,
|
|
18
18
|
WarehouseService,
|
|
19
19
|
Type,
|
|
20
|
-
SellerAddress,
|
|
21
20
|
OriginalOrder,
|
|
22
21
|
ModifiedOrder,
|
|
23
22
|
ShipmentType,
|
|
24
23
|
IntegrationType,
|
|
25
24
|
UserDefaultConfigurationContent,
|
|
26
25
|
GetModifiedOrderBasedOnRules,
|
|
27
|
-
IsPriority,
|
|
28
26
|
CustomizationService,
|
|
29
27
|
InsertContentShipmentLine,
|
|
30
|
-
Bulto,
|
|
31
28
|
DataToInsert,
|
|
32
29
|
BultoYCantidad,
|
|
33
30
|
JsonReferenciasCantidades,
|
|
34
|
-
|
|
31
|
+
InsertContentShipmentDetail
|
|
35
32
|
} from './types';
|
|
36
33
|
|
|
37
34
|
interface Services {
|
|
@@ -163,12 +160,15 @@ export class IntegrationManager {
|
|
|
163
160
|
|
|
164
161
|
const isPriorityByDefault = await checkIsPriorityByDefault();
|
|
165
162
|
|
|
163
|
+
const abortedTempShipments: number[] = [];
|
|
164
|
+
|
|
166
165
|
const insertOrdersResult = await new this.executionManager({
|
|
167
166
|
arrayParams: filteredOrders,
|
|
168
167
|
executable: async (order: OriginalOrder) => {
|
|
169
168
|
const parsedOrder = await modificarOrdenOriginal({
|
|
170
169
|
originalOrder: order
|
|
171
170
|
});
|
|
171
|
+
|
|
172
172
|
let direccionSalida = null;
|
|
173
173
|
if (parsedOrder.direccionSalidaId) {
|
|
174
174
|
direccionSalida = await addressesService.getAddress(
|
|
@@ -182,7 +182,7 @@ export class IntegrationManager {
|
|
|
182
182
|
shipmentType: this.shipmentType,
|
|
183
183
|
agencyId,
|
|
184
184
|
warehouseId,
|
|
185
|
-
sellerAddress,
|
|
185
|
+
sellerAddress: direccionSalida ?? sellerAddress,
|
|
186
186
|
idUsuario,
|
|
187
187
|
findNextPickupDate: this.findNextPickupDate,
|
|
188
188
|
countriesService: this.services.countriesService,
|
|
@@ -199,20 +199,13 @@ export class IntegrationManager {
|
|
|
199
199
|
throw new Error('Temp shipments could not be created');
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const shipmentDetails = this.dataToInsert.details.filter(
|
|
212
|
-
({ id_envio }) => {
|
|
213
|
-
return id_envio === order.idEnvioTemporal;
|
|
214
|
-
}
|
|
215
|
-
);
|
|
202
|
+
const { shipmentDetails, bultosYCantidades } =
|
|
203
|
+
await this.insertQuantityRelatedLines({
|
|
204
|
+
parsedOrder: { ...parsedOrder, idEnvioTemporal },
|
|
205
|
+
idUsuario,
|
|
206
|
+
crearBulto,
|
|
207
|
+
group
|
|
208
|
+
});
|
|
216
209
|
|
|
217
210
|
const { partialTempShipmentModified, packagesModified } =
|
|
218
211
|
await this.getModifiedOrderBasedOnRules({
|
|
@@ -242,6 +235,8 @@ export class IntegrationManager {
|
|
|
242
235
|
id_usuario: idUsuario
|
|
243
236
|
});
|
|
244
237
|
|
|
238
|
+
abortedTempShipments.push(idEnvioTemporal);
|
|
239
|
+
|
|
245
240
|
this.dataToInsert.lines.delete(idEnvioTemporal);
|
|
246
241
|
|
|
247
242
|
return;
|
|
@@ -252,6 +247,7 @@ export class IntegrationManager {
|
|
|
252
247
|
|
|
253
248
|
await integrationsService.updateTempShimpment({
|
|
254
249
|
id_envio: idEnvioTemporal,
|
|
250
|
+
id_usuario: idUsuario,
|
|
255
251
|
...partialTempShipmentModified
|
|
256
252
|
});
|
|
257
253
|
}
|
|
@@ -260,18 +256,58 @@ export class IntegrationManager {
|
|
|
260
256
|
this.dataToInsert.lines.set(idEnvioTemporal, packagesModified);
|
|
261
257
|
}
|
|
262
258
|
|
|
259
|
+
// para cuando el usuario quiere mapear su sku con la referencia que tenga en almacen
|
|
260
|
+
if (shouldFindWarehouseSkus) {
|
|
261
|
+
const bultosYCantidadesGroupedByShipment = bultosYCantidades.reduce<
|
|
262
|
+
Record<number, BultoYCantidad[]>
|
|
263
|
+
>((acc, curr) => {
|
|
264
|
+
const tempShipmentId = curr?.idEnvioTemporal;
|
|
265
|
+
if (!tempShipmentId) return acc;
|
|
266
|
+
|
|
267
|
+
acc[tempShipmentId] ??= [];
|
|
268
|
+
acc[tempShipmentId].push(curr);
|
|
269
|
+
|
|
270
|
+
return acc;
|
|
271
|
+
}, {});
|
|
272
|
+
|
|
273
|
+
await Promise.all(
|
|
274
|
+
Object.entries(bultosYCantidadesGroupedByShipment).map(
|
|
275
|
+
async ([tempShipmentId, bultosYCantidades]) => {
|
|
276
|
+
const json_referencias_cantidades =
|
|
277
|
+
await this.createJsonQuantityReferencesFromLines({
|
|
278
|
+
idUsuario,
|
|
279
|
+
warehouseId,
|
|
280
|
+
bultosYCantidades
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (json_referencias_cantidades.length > 0) {
|
|
284
|
+
this.dataToInsert.jsonQuantityReferences.push({
|
|
285
|
+
id_envio: Number(tempShipmentId),
|
|
286
|
+
json_referencias_cantidades
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// external
|
|
263
295
|
this.insertExternalShipment({
|
|
264
296
|
idUsuario,
|
|
265
297
|
order: parsedOrder,
|
|
266
298
|
idEnvioTemporal
|
|
267
299
|
});
|
|
268
300
|
|
|
301
|
+
// details
|
|
302
|
+
this.dataToInsert.details.push(...shipmentDetails);
|
|
303
|
+
|
|
269
304
|
return Object.assign(parsedOrder, { idEnvioTemporal, agencyId });
|
|
270
305
|
},
|
|
271
306
|
regularExecutionTime: 8000, // ms
|
|
272
307
|
initialBatchQuantity: sequentialInsertion ? 1 : 5 // si la insercion no es secuencial, de cinco en cinco
|
|
273
308
|
}).upload();
|
|
274
309
|
|
|
310
|
+
// content (dentro de esta funcion coge los detalles y evoluciona a partir de ahi)
|
|
275
311
|
await this.insertUserDefaultConfigurationContent({
|
|
276
312
|
user,
|
|
277
313
|
countryName: country.nombre_pais_en
|
|
@@ -430,28 +466,6 @@ export class IntegrationManager {
|
|
|
430
466
|
});
|
|
431
467
|
};
|
|
432
468
|
|
|
433
|
-
insertTempShipmentGroupLine = ({
|
|
434
|
-
idEnvioTemporal,
|
|
435
|
-
group
|
|
436
|
-
}: {
|
|
437
|
-
idEnvioTemporal: number;
|
|
438
|
-
group: Group;
|
|
439
|
-
}) => {
|
|
440
|
-
// si agrupamos, no deberia de haber mas lineas, pero por si acaso, con el set machacamos lo que pudiera haber -que no deberia haber nada, insisto-
|
|
441
|
-
this.dataToInsert.lines.set(idEnvioTemporal, [
|
|
442
|
-
{
|
|
443
|
-
id_envio: idEnvioTemporal,
|
|
444
|
-
bulto: {
|
|
445
|
-
peso: group.weight,
|
|
446
|
-
alto: group.height,
|
|
447
|
-
ancho: group.width,
|
|
448
|
-
largo: group.length,
|
|
449
|
-
sku_producto: ''
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
]);
|
|
453
|
-
};
|
|
454
|
-
|
|
455
469
|
async createJsonQuantityReferencesFromLines({
|
|
456
470
|
idUsuario,
|
|
457
471
|
warehouseId,
|
|
@@ -503,20 +517,19 @@ export class IntegrationManager {
|
|
|
503
517
|
}
|
|
504
518
|
|
|
505
519
|
async insertQuantityRelatedLines({
|
|
506
|
-
|
|
520
|
+
parsedOrder,
|
|
507
521
|
idUsuario,
|
|
508
522
|
crearBulto,
|
|
509
|
-
group
|
|
510
|
-
warehouseId,
|
|
511
|
-
shouldFindWarehouseSkus
|
|
523
|
+
group
|
|
512
524
|
}: {
|
|
513
|
-
|
|
525
|
+
parsedOrder: ModifiedOrderExtended;
|
|
514
526
|
idUsuario: number;
|
|
515
527
|
crearBulto: CrearBulto;
|
|
516
528
|
group: Group;
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
529
|
+
}): Promise<{
|
|
530
|
+
shipmentDetails: InsertContentShipmentDetail[];
|
|
531
|
+
bultosYCantidades: BultoYCantidad[];
|
|
532
|
+
}> {
|
|
520
533
|
/*
|
|
521
534
|
En esta funcion se calculan los records a incluir en la tabla de detalles y en las de lineas
|
|
522
535
|
Los detalles, es facil, una por cada bulto (aqui no hay ni "agrupar" ni nada de eso, una por cada bulto y ya esta)
|
|
@@ -528,6 +541,8 @@ export class IntegrationManager {
|
|
|
528
541
|
|
|
529
542
|
const { integrationsService } = this.services;
|
|
530
543
|
|
|
544
|
+
const shipmentDetails: InsertContentShipmentDetail[] = [];
|
|
545
|
+
|
|
531
546
|
const shouldGetMeasuresFromGroupBeingOneLine = !!(
|
|
532
547
|
await integrationsService.getDefaultIntegrationsData({
|
|
533
548
|
id_usuario: idUsuario,
|
|
@@ -561,122 +576,97 @@ export class IntegrationManager {
|
|
|
561
576
|
return null;
|
|
562
577
|
};
|
|
563
578
|
|
|
564
|
-
|
|
565
|
-
// por cada pedido/envio
|
|
566
|
-
orders.map(async ({ idEnvioTemporal, lineas }) => {
|
|
567
|
-
if (!idEnvioTemporal) return;
|
|
579
|
+
// por cada pedido/envio
|
|
568
580
|
|
|
569
|
-
|
|
570
|
-
const bultosYCantidades = (
|
|
571
|
-
await Promise.all(
|
|
572
|
-
lineas.map(async (line) => {
|
|
573
|
-
const bultoYCantidad = await createBultoFromLine({
|
|
574
|
-
line,
|
|
575
|
-
idEnvioTemporal
|
|
576
|
-
});
|
|
577
|
-
return bultoYCantidad;
|
|
578
|
-
})
|
|
579
|
-
)
|
|
580
|
-
).filter(Boolean);
|
|
581
|
-
|
|
582
|
-
// sacamos todos los bultos (y cantidad)
|
|
583
|
-
await Promise.allSettled(
|
|
584
|
-
bultosYCantidades.map(async (bultoYCantidad) => {
|
|
585
|
-
if (!bultoYCantidad) return;
|
|
586
|
-
|
|
587
|
-
const {
|
|
588
|
-
bulto,
|
|
589
|
-
cantidad,
|
|
590
|
-
idEnvioTemporal,
|
|
591
|
-
forzarAgrupamientoDeLinea
|
|
592
|
-
} = bultoYCantidad;
|
|
593
|
-
|
|
594
|
-
// metemos los detalles siempre
|
|
595
|
-
this.dataToInsert.details.push({
|
|
596
|
-
id_envio: idEnvioTemporal,
|
|
597
|
-
bulto,
|
|
598
|
-
cantidad
|
|
599
|
-
});
|
|
581
|
+
const { idEnvioTemporal, lineas } = parsedOrder;
|
|
600
582
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (shouldGroup) {
|
|
614
|
-
this.insertTempShipmentGroupLine({ idEnvioTemporal, group });
|
|
615
|
-
// si no agrupamos, cogemos el peso y dimensiones del peso mismo
|
|
616
|
-
} else {
|
|
617
|
-
// lo de "forzarAgrupamientoDeLinea" se hizo solo para un usuario (Pegaso) que ya ni siquiera esta. Lo que hace es que siempre mete una linea, sin tener en cuenta la cantidad, pero con las dimensiones que les hemos preparado para ese bulto (que es una suma de todos los bultos reales... un chanchullo)
|
|
618
|
-
const tempShipmentLines = [];
|
|
619
|
-
|
|
620
|
-
if (forzarAgrupamientoDeLinea) {
|
|
621
|
-
tempShipmentLines.push({
|
|
622
|
-
id_envio: idEnvioTemporal,
|
|
623
|
-
bulto
|
|
624
|
-
});
|
|
625
|
-
// y esto para todo el resto de usuarios
|
|
626
|
-
} else {
|
|
627
|
-
// creamos una linea por cada una de las unidades de "cantidad" (si cantidad es 5, pues 5 lineas -todas iguales, con el peso y medidas del bulto-)
|
|
628
|
-
|
|
629
|
-
const arrayFake = Array.from({ length: Number(cantidad) });
|
|
630
|
-
const linesToAdd = arrayFake.map(() => ({
|
|
631
|
-
id_envio: idEnvioTemporal,
|
|
632
|
-
bulto
|
|
633
|
-
}));
|
|
634
|
-
|
|
635
|
-
tempShipmentLines.push(...linesToAdd);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
this.dataToInsert.lines.set(idEnvioTemporal, tempShipmentLines);
|
|
639
|
-
}
|
|
583
|
+
// hacemos esto primero, todavia no sabemos si vamos a agrupar (porque necesitamos crear el bulto para eso, ya que la "cantidad" podria influir)
|
|
584
|
+
const bultosYCantidades = (
|
|
585
|
+
await Promise.all(
|
|
586
|
+
lineas.map(async (line) => {
|
|
587
|
+
const bultoYCantidad = await createBultoFromLine({
|
|
588
|
+
line,
|
|
589
|
+
idEnvioTemporal
|
|
590
|
+
});
|
|
591
|
+
return bultoYCantidad;
|
|
592
|
+
})
|
|
593
|
+
)
|
|
594
|
+
).filter(Boolean);
|
|
640
595
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
(acc, curr) => {
|
|
646
|
-
const tempShipmentId = curr?.idEnvioTemporal;
|
|
647
|
-
if (!tempShipmentId) return acc;
|
|
596
|
+
// sacamos todos los bultos (y cantidad)
|
|
597
|
+
await Promise.allSettled(
|
|
598
|
+
bultosYCantidades.map(async (bultoYCantidad) => {
|
|
599
|
+
if (!bultoYCantidad) return;
|
|
648
600
|
|
|
649
|
-
|
|
650
|
-
|
|
601
|
+
const { bulto, cantidad, idEnvioTemporal, forzarAgrupamientoDeLinea } =
|
|
602
|
+
bultoYCantidad;
|
|
651
603
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
604
|
+
// metemos los detalles siempre
|
|
605
|
+
shipmentDetails.push({
|
|
606
|
+
id_envio: idEnvioTemporal,
|
|
607
|
+
bulto,
|
|
608
|
+
cantidad
|
|
609
|
+
});
|
|
656
610
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
611
|
+
// Debe agrupar si:
|
|
612
|
+
// 1. grouped es 1 + hay más de una línea
|
|
613
|
+
// 2. grouped es 1 + hay una línea + configuración integraciones tiene el campo dimensiones_medidas_configuradas a 1 (osea, que queremos que coja las dimensiones del bulto, aunque sea una sola linea)
|
|
614
|
+
// 3. grouped es 1 + hay una lines + cantidad es mayor que 1
|
|
615
|
+
const shouldGroup =
|
|
616
|
+
group?.grouped === 1 &&
|
|
617
|
+
(lineas.length > 1 ||
|
|
618
|
+
(lineas.length === 1 && shouldGetMeasuresFromGroupBeingOneLine) ||
|
|
619
|
+
(lineas.length === 1 && Number(cantidad) > 1));
|
|
620
|
+
|
|
621
|
+
// si agrupamos, metemos solo una linea en "lineas" y cogemos las dimensiones y peso de "group" (lo que el usuario tenga por defecto)
|
|
622
|
+
if (shouldGroup) {
|
|
623
|
+
// si agrupamos, no deberia de haber mas lineas, pero por si acaso, con el set machacamos lo que pudiera haber -que no deberia haber nada, insisto-
|
|
624
|
+
this.dataToInsert.lines.set(idEnvioTemporal, [
|
|
625
|
+
{
|
|
626
|
+
id_envio: idEnvioTemporal,
|
|
627
|
+
bulto: {
|
|
628
|
+
peso: group.weight,
|
|
629
|
+
alto: group.height,
|
|
630
|
+
ancho: group.width,
|
|
631
|
+
largo: group.length,
|
|
632
|
+
sku_producto: ''
|
|
633
|
+
}
|
|
676
634
|
}
|
|
677
|
-
|
|
678
|
-
|
|
635
|
+
]);
|
|
636
|
+
// si no agrupamos, cogemos el peso y dimensiones del peso mismo
|
|
637
|
+
} else {
|
|
638
|
+
// lo de "forzarAgrupamientoDeLinea" se hizo solo para un usuario (Pegaso) que ya ni siquiera esta. Lo que hace es que siempre mete una linea, sin tener en cuenta la cantidad, pero con las dimensiones que les hemos preparado para ese bulto (que es una suma de todos los bultos reales... un chanchullo)
|
|
639
|
+
const tempShipmentLines = [];
|
|
640
|
+
|
|
641
|
+
if (forzarAgrupamientoDeLinea) {
|
|
642
|
+
tempShipmentLines.push({
|
|
643
|
+
id_envio: idEnvioTemporal,
|
|
644
|
+
bulto
|
|
645
|
+
});
|
|
646
|
+
// y esto para todo el resto de usuarios
|
|
647
|
+
} else {
|
|
648
|
+
// creamos una linea por cada una de las unidades de "cantidad" (si cantidad es 5, pues 5 lineas -todas iguales, con el peso y medidas del bulto-)
|
|
649
|
+
|
|
650
|
+
const arrayFake = Array.from({ length: Number(cantidad) });
|
|
651
|
+
const linesToAdd = arrayFake.map(() => ({
|
|
652
|
+
id_envio: idEnvioTemporal,
|
|
653
|
+
bulto
|
|
654
|
+
}));
|
|
655
|
+
|
|
656
|
+
tempShipmentLines.push(...linesToAdd);
|
|
657
|
+
}
|
|
658
|
+
if (this.dataToInsert.lines.has(idEnvioTemporal)) {
|
|
659
|
+
this.dataToInsert.lines.set(idEnvioTemporal, [
|
|
660
|
+
...(this.dataToInsert.lines.get(idEnvioTemporal) ?? []),
|
|
661
|
+
...tempShipmentLines
|
|
662
|
+
]);
|
|
663
|
+
} else {
|
|
664
|
+
this.dataToInsert.lines.set(idEnvioTemporal, tempShipmentLines);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
679
667
|
})
|
|
680
668
|
);
|
|
669
|
+
|
|
670
|
+
return { shipmentDetails, bultosYCantidades };
|
|
681
671
|
}
|
|
682
672
|
}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
roots: ['<rootDir>/__tests__'],
|
|
6
|
+
testMatch: ['**/*.test.ts'],
|
|
7
|
+
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
|
|
8
|
+
modulePathIgnorePatterns: ['<rootDir>/dist/'],
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.tsx?$': [
|
|
11
|
+
'ts-jest',
|
|
12
|
+
{
|
|
13
|
+
diagnostics: false
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cardus",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.106",
|
|
4
4
|
"description": "an integration manager",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"license": "ISC",
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@eslint/js": "^9.1.1",
|
|
18
|
+
"@types/jest": "^30.0.0",
|
|
18
19
|
"@types/node": "^20.12.7",
|
|
19
20
|
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
|
20
21
|
"@typescript-eslint/parser": "^7.7.1",
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"globals": "^15.0.0",
|
|
23
24
|
"jest": "^29.7.0",
|
|
24
25
|
"prettier": "^3.2.5",
|
|
26
|
+
"ts-jest": "^29.4.9",
|
|
25
27
|
"typescript": "^5.4.5",
|
|
26
28
|
"typescript-eslint": "^7.7.1"
|
|
27
29
|
},
|
package/types/index.ts
CHANGED
|
@@ -460,7 +460,7 @@ type InsertContentShipmentData = UserDefaultConfigurationContent & {
|
|
|
460
460
|
id_envio: number;
|
|
461
461
|
};
|
|
462
462
|
|
|
463
|
-
type InsertContentShipmentDetail = {
|
|
463
|
+
export type InsertContentShipmentDetail = {
|
|
464
464
|
id_envio: number;
|
|
465
465
|
bulto: Bulto['bulto'];
|
|
466
466
|
cantidad: number;
|