mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
feat(bookings): surface cargo declared on the shipment request
A GENERAL + customs contract does not let the customer book directly: they
submit a shipment request, and initiateForShipmentRequest opens a BARE booking
from it — "the request itself carries the quantities; the instance carries
none". Between initiation and completeUnderContract the booking legitimately
holds no cargo, so the export reported 0 containers for a customer who had
declared, say, 2 x 20FT. 23 bookings on dev data are in that state.
Adds two columns and one filter reading booking_requests.requested_lines:
- "Requested cargo" — the declared lines as text ("2 x 20FT"), handling the
bulk shape too (tons / item count), not only containers.
- "Requested containers" — the declared box count, with a matching min/max
filter on the list and the export.
Deliberately a separate column rather than a fallback inside the real container
count: a declared 2 x 20FT is a request, not two boxes on a booking, and
merging them would overstate operational totals. The two compose instead —
Containers = 0 AND Requested containers >= 1 is exactly the set awaiting
completion after clearance.
requested_lines is free-form jsonb, so the container array is guarded by
jsonb_typeof before jsonb_array_elements; one malformed row would otherwise
500 the whole list.
This commit is contained in:
@@ -5,6 +5,8 @@ import {
|
|||||||
bookingContentMatchSql,
|
bookingContentMatchSql,
|
||||||
bookingContentSql,
|
bookingContentSql,
|
||||||
bookingHasContainerTypeSql,
|
bookingHasContainerTypeSql,
|
||||||
|
bookingRequestedCargoSql,
|
||||||
|
bookingRequestedContainerCountSql,
|
||||||
} from './booking-content.sql';
|
} from './booking-content.sql';
|
||||||
|
|
||||||
describe('bookingContentSql', () => {
|
describe('bookingContentSql', () => {
|
||||||
@@ -106,3 +108,39 @@ describe('bookingContainerVgmSql', () => {
|
|||||||
expect(sql).toContain('bc.deleted_at IS NULL');
|
expect(sql).toContain('bc.deleted_at IS NULL');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('requested (shipment-request) cargo', () => {
|
||||||
|
const cargo = bookingRequestedCargoSql('b');
|
||||||
|
const count = bookingRequestedContainerCountSql('b');
|
||||||
|
|
||||||
|
it('reads the request, never the booking or its container lines', () => {
|
||||||
|
for (const sql of [cargo, count]) {
|
||||||
|
expect(sql).toContain('freight.booking_requests br');
|
||||||
|
expect(sql).toContain('br.created_booking_id = b.id');
|
||||||
|
expect(sql).not.toContain('freight.booking_container');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// requested_lines is a free-form jsonb column; jsonb_array_elements throws on
|
||||||
|
// a non-array, which would 500 the whole list for one malformed row.
|
||||||
|
it('survives a requested_lines with no container array', () => {
|
||||||
|
for (const sql of [cargo, count]) {
|
||||||
|
expect(sql).toContain("jsonb_typeof(br.requested_lines->'containers') = 'array'");
|
||||||
|
expect(sql).toContain("ELSE '[]'::jsonb");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the bulk shape too, not only containers', () => {
|
||||||
|
expect(cargo).toContain("'bulk'->>'cargoWeightTons'");
|
||||||
|
expect(cargo).toContain("'bulk'->>'itemCount'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts 0 rather than NULL when no request exists', () => {
|
||||||
|
expect(count).toContain("COALESCE(SUM((l->>'quantity')::int), 0)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores soft-deleted requests', () => {
|
||||||
|
expect(cargo).toContain('br.deleted_at IS NULL');
|
||||||
|
expect(count).toContain('br.deleted_at IS NULL');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -98,3 +98,51 @@ export function bookingContainerVgmSql(alias = 'b'): string {
|
|||||||
WHERE bc.booking_id = ${alias}.id
|
WHERE bc.booking_id = ${alias}.id
|
||||||
AND bc.deleted_at IS NULL)`;
|
AND bc.deleted_at IS NULL)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cargo the customer declared on the SHIPMENT REQUEST behind a booking, which
|
||||||
|
* is not the same fact as cargo on the booking itself.
|
||||||
|
*
|
||||||
|
* On a GENERAL + customs contract the customer cannot book directly: they
|
||||||
|
* submit a request (day + quantities), and `initiateForShipmentRequest` opens a
|
||||||
|
* BARE instance from it — "the request itself carries the quantities; the
|
||||||
|
* instance carries none". So between initiation and `completeUnderContract` the
|
||||||
|
* booking legitimately holds no cargo while the customer's declared quantities
|
||||||
|
* sit on `booking_requests.requested_lines`.
|
||||||
|
*
|
||||||
|
* Kept in its own column rather than folded into the real container count: a
|
||||||
|
* declared 2 × 20FT is a request, not two boxes on a booking, and merging the
|
||||||
|
* two would overstate operational totals.
|
||||||
|
*/
|
||||||
|
const REQUESTED_CONTAINER_LINES = `jsonb_array_elements(
|
||||||
|
CASE WHEN jsonb_typeof(br.requested_lines->'containers') = 'array'
|
||||||
|
THEN br.requested_lines->'containers'
|
||||||
|
ELSE '[]'::jsonb END)`;
|
||||||
|
|
||||||
|
/** Human-readable declared cargo: "2 × 20FT", "12 t", "40 items". */
|
||||||
|
export function bookingRequestedCargoSql(alias = 'b'): string {
|
||||||
|
return `(SELECT COALESCE(
|
||||||
|
(SELECT string_agg((l->>'quantity') || ' × ' || upper(l->>'containerSize'), ', '
|
||||||
|
ORDER BY l->>'containerSize')
|
||||||
|
FROM ${REQUESTED_CONTAINER_LINES} AS l),
|
||||||
|
NULLIF(br.requested_lines->'bulk'->>'cargoWeightTons', '') || ' t',
|
||||||
|
NULLIF(br.requested_lines->'bulk'->>'itemCount', '') || ' items')
|
||||||
|
FROM freight.booking_requests br
|
||||||
|
WHERE br.created_booking_id = ${alias}.id
|
||||||
|
AND br.deleted_at IS NULL
|
||||||
|
ORDER BY br.created_at DESC
|
||||||
|
LIMIT 1)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boxes declared on the shipment request. Pairs with the real container count:
|
||||||
|
* `Containers = 0` AND `Requested containers >= 1` is exactly the set awaiting
|
||||||
|
* completion.
|
||||||
|
*/
|
||||||
|
export function bookingRequestedContainerCountSql(alias = 'b'): string {
|
||||||
|
return `(SELECT COALESCE(SUM((l->>'quantity')::int), 0)
|
||||||
|
FROM freight.booking_requests br
|
||||||
|
CROSS JOIN LATERAL ${REQUESTED_CONTAINER_LINES} AS l
|
||||||
|
WHERE br.created_booking_id = ${alias}.id
|
||||||
|
AND br.deleted_at IS NULL)`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
bookingContainerCountSql,
|
bookingContainerCountSql,
|
||||||
bookingContentMatchSql,
|
bookingContentMatchSql,
|
||||||
bookingHasContainerTypeSql,
|
bookingHasContainerTypeSql,
|
||||||
|
bookingRequestedContainerCountSql,
|
||||||
} from './booking-content.sql';
|
} from './booking-content.sql';
|
||||||
import { BookingCargoModifier } from './entities/booking-cargo-modifier.entity';
|
import { BookingCargoModifier } from './entities/booking-cargo-modifier.entity';
|
||||||
import {
|
import {
|
||||||
@@ -79,6 +80,9 @@ export interface BookingListFilterOptions {
|
|||||||
containerTypeId?: string;
|
containerTypeId?: string;
|
||||||
containersMin?: number;
|
containersMin?: number;
|
||||||
containersMax?: number;
|
containersMax?: number;
|
||||||
|
/** Bounds on containers declared on the shipment request behind the booking. */
|
||||||
|
requestedContainersMin?: number;
|
||||||
|
requestedContainersMax?: number;
|
||||||
freightType?: string;
|
freightType?: string;
|
||||||
bookingType?: string;
|
bookingType?: string;
|
||||||
tradeDirection?: string;
|
tradeDirection?: string;
|
||||||
@@ -1225,6 +1229,19 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Declared on the shipment request, not on the booking. Pairs with the
|
||||||
|
// count above: containers 0..0 AND requested >= 1 is the set awaiting
|
||||||
|
// completion after clearance.
|
||||||
|
if (options.requestedContainersMin != null) {
|
||||||
|
qb.andWhere(`${bookingRequestedContainerCountSql('booking')} >= :requestedContainersMin`, {
|
||||||
|
requestedContainersMin: options.requestedContainersMin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (options.requestedContainersMax != null) {
|
||||||
|
qb.andWhere(`${bookingRequestedContainerCountSql('booking')} <= :requestedContainersMax`, {
|
||||||
|
requestedContainersMax: options.requestedContainersMax,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (omit !== 'freightType' && options.freightType) {
|
if (omit !== 'freightType' && options.freightType) {
|
||||||
qb.andWhere('booking.freight_type = :freightType', {
|
qb.andWhere('booking.freight_type = :freightType', {
|
||||||
freightType: options.freightType,
|
freightType: options.freightType,
|
||||||
|
|||||||
@@ -1849,6 +1849,8 @@ export class BookingsService {
|
|||||||
containerTypeId: filter.containerTypeId,
|
containerTypeId: filter.containerTypeId,
|
||||||
containersMin: filter.containersMin,
|
containersMin: filter.containersMin,
|
||||||
containersMax: filter.containersMax,
|
containersMax: filter.containersMax,
|
||||||
|
requestedContainersMin: filter.requestedContainersMin,
|
||||||
|
requestedContainersMax: filter.requestedContainersMax,
|
||||||
freightType: filter.freightType,
|
freightType: filter.freightType,
|
||||||
bookingType: filter.bookingType,
|
bookingType: filter.bookingType,
|
||||||
tradeDirection: filter.tradeDirection,
|
tradeDirection: filter.tradeDirection,
|
||||||
@@ -2080,6 +2082,8 @@ export class BookingsService {
|
|||||||
containerTypeId: filter.containerTypeId,
|
containerTypeId: filter.containerTypeId,
|
||||||
containersMin: filter.containersMin,
|
containersMin: filter.containersMin,
|
||||||
containersMax: filter.containersMax,
|
containersMax: filter.containersMax,
|
||||||
|
requestedContainersMin: filter.requestedContainersMin,
|
||||||
|
requestedContainersMax: filter.requestedContainersMax,
|
||||||
freightType: filter.freightType,
|
freightType: filter.freightType,
|
||||||
bookingType: filter.bookingType,
|
bookingType: filter.bookingType,
|
||||||
tradeDirection: filter.tradeDirection,
|
tradeDirection: filter.tradeDirection,
|
||||||
|
|||||||
@@ -103,6 +103,19 @@ export class FilterBookingDto {
|
|||||||
@Transform(({ value }) => (value === '' || value == null ? undefined : Number(value)))
|
@Transform(({ value }) => (value === '' || value == null ? undefined : Number(value)))
|
||||||
containersMax?: number;
|
containersMax?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
'Minimum containers declared on the shipment request behind the booking',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => (value === '' || value == null ? undefined : Number(value)))
|
||||||
|
requestedContainersMin?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Maximum requested containers — see requestedContainersMin' })
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => (value === '' || value == null ? undefined : Number(value)))
|
||||||
|
requestedContainersMax?: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: FREIGHT_TYPES })
|
@ApiPropertyOptional({ enum: FREIGHT_TYPES })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn([...FREIGHT_TYPES])
|
@IsIn([...FREIGHT_TYPES])
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
bookingContainerVgmSql,
|
bookingContainerVgmSql,
|
||||||
bookingContentSql,
|
bookingContentSql,
|
||||||
bookingHasContainerTypeSql,
|
bookingHasContainerTypeSql,
|
||||||
|
bookingRequestedCargoSql,
|
||||||
|
bookingRequestedContainerCountSql,
|
||||||
} from '../../bookings/booking-content.sql';
|
} from '../../bookings/booking-content.sql';
|
||||||
import { bookingTonsSql } from '../../bookings/booking-tons.sql';
|
import { bookingTonsSql } from '../../bookings/booking-tons.sql';
|
||||||
import { Booking } from '../../bookings/entities/booking.entity';
|
import { Booking } from '../../bookings/entities/booking.entity';
|
||||||
@@ -36,6 +38,7 @@ const REVENUE = 'COALESCE(b.adjusted_total_amount, b.total_amount)';
|
|||||||
/** What the customer described as the booking's contents — see the helper. */
|
/** What the customer described as the booking's contents — see the helper. */
|
||||||
const CONTENT = bookingContentSql('b');
|
const CONTENT = bookingContentSql('b');
|
||||||
const CONTAINER_COUNT = bookingContainerCountSql('b');
|
const CONTAINER_COUNT = bookingContainerCountSql('b');
|
||||||
|
const REQUESTED_COUNT = bookingRequestedContainerCountSql('b');
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
const STATUS_OPTIONS = [
|
||||||
'DRAFT', 'SUBMITTED', 'UNDER_REVIEW', 'APPROVED', 'REJECTED',
|
'DRAFT', 'SUBMITTED', 'UNDER_REVIEW', 'APPROVED', 'REJECTED',
|
||||||
@@ -210,6 +213,9 @@ export const bookingsDataset: ExportDataset = {
|
|||||||
{ key: 'cargoDescription', label: 'Cargo description', type: 'string', group: 'cargo', select: 'b.cargo_free_text' },
|
{ key: 'cargoDescription', label: 'Cargo description', type: 'string', group: 'cargo', select: 'b.cargo_free_text' },
|
||||||
// Boxes, not lines: booking_container is one row per LINE with a quantity.
|
// Boxes, not lines: booking_container is one row per LINE with a quantity.
|
||||||
{ key: 'containerCount', label: 'Containers', type: 'number', group: 'cargo', default: true, select: `${CONTAINER_COUNT}::int`, sortExpr: CONTAINER_COUNT },
|
{ key: 'containerCount', label: 'Containers', type: 'number', group: 'cargo', default: true, select: `${CONTAINER_COUNT}::int`, sortExpr: CONTAINER_COUNT },
|
||||||
|
// Declared on the shipment request, not yet on the booking — see the helper.
|
||||||
|
{ key: 'requestedCargo', label: 'Requested cargo', type: 'string', group: 'cargo', select: bookingRequestedCargoSql('b') },
|
||||||
|
{ key: 'requestedContainers', label: 'Requested containers', type: 'number', group: 'cargo', select: `${REQUESTED_COUNT}::int`, sortExpr: REQUESTED_COUNT },
|
||||||
{ key: 'freightType', label: 'Freight type', type: 'string', group: 'cargo', default: true, select: 'b.freight_type' },
|
{ key: 'freightType', label: 'Freight type', type: 'string', group: 'cargo', default: true, select: 'b.freight_type' },
|
||||||
{ key: 'tons', label: 'Tonnage', type: 'tons', group: 'cargo', default: true, select: `ROUND(${TONS})::float8`, sortExpr: TONS },
|
{ key: 'tons', label: 'Tonnage', type: 'tons', group: 'cargo', default: true, select: `ROUND(${TONS})::float8`, sortExpr: TONS },
|
||||||
// The per-line sum, NOT b.cargo_total_weight_vgm — the portal leaves that
|
// The per-line sum, NOT b.cargo_total_weight_vgm — the portal leaves that
|
||||||
@@ -296,6 +302,8 @@ export const bookingsDataset: ExportDataset = {
|
|||||||
{ key: 'containerTypeId', label: 'Container type', type: 'select', optionsQuery: containerTypeOptions },
|
{ key: 'containerTypeId', label: 'Container type', type: 'select', optionsQuery: containerTypeOptions },
|
||||||
{ key: 'containersMin', label: 'Containers (min)', type: 'text' },
|
{ key: 'containersMin', label: 'Containers (min)', type: 'text' },
|
||||||
{ key: 'containersMax', label: 'Containers (max)', type: 'text' },
|
{ key: 'containersMax', label: 'Containers (max)', type: 'text' },
|
||||||
|
{ key: 'requestedContainersMin', label: 'Requested containers (min)', type: 'text' },
|
||||||
|
{ key: 'requestedContainersMax', label: 'Requested containers (max)', type: 'text' },
|
||||||
{ key: 'companyId', label: 'Customer', type: 'text' },
|
{ key: 'companyId', label: 'Customer', type: 'text' },
|
||||||
{ key: 'search', label: 'Search reference or customer', type: 'text' },
|
{ key: 'search', label: 'Search reference or customer', type: 'text' },
|
||||||
],
|
],
|
||||||
@@ -329,6 +337,10 @@ export const bookingsDataset: ExportDataset = {
|
|||||||
const max = num(params.containersMax);
|
const max = num(params.containersMax);
|
||||||
if (Number.isFinite(min)) qb.andWhere(`${containerCount} >= :containersMin`, { containersMin: min });
|
if (Number.isFinite(min)) qb.andWhere(`${containerCount} >= :containersMin`, { containersMin: min });
|
||||||
if (Number.isFinite(max)) qb.andWhere(`${containerCount} <= :containersMax`, { containersMax: max });
|
if (Number.isFinite(max)) qb.andWhere(`${containerCount} <= :containersMax`, { containersMax: max });
|
||||||
|
const reqMin = num(params.requestedContainersMin);
|
||||||
|
const reqMax = num(params.requestedContainersMax);
|
||||||
|
if (Number.isFinite(reqMin)) qb.andWhere(`${REQUESTED_COUNT} >= :requestedContainersMin`, { requestedContainersMin: reqMin });
|
||||||
|
if (Number.isFinite(reqMax)) qb.andWhere(`${REQUESTED_COUNT} <= :requestedContainersMax`, { requestedContainersMax: reqMax });
|
||||||
if (params.paymentStatus) qb.andWhere('b.payment_status = :paymentStatus', { paymentStatus: params.paymentStatus });
|
if (params.paymentStatus) qb.andWhere('b.payment_status = :paymentStatus', { paymentStatus: params.paymentStatus });
|
||||||
if (params.companyId) qb.andWhere('b.company_id = :companyId', { companyId: params.companyId });
|
if (params.companyId) qb.andWhere('b.company_id = :companyId', { companyId: params.companyId });
|
||||||
if (params.search) {
|
if (params.search) {
|
||||||
|
|||||||
@@ -251,6 +251,25 @@ export default function BookingRequestsPage() {
|
|||||||
? { containersMin: v.v[0], containersMax: v.v[1] }
|
? { containersMin: v.v[0], containersMax: v.v[1] }
|
||||||
: { containersMin: v.v[0], containersMax: v.v[0] },
|
: { containersMin: v.v[0], containersMax: v.v[0] },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Declared on the shipment request, not yet on the booking. Pair it
|
||||||
|
// with Containers = 0 to find the set awaiting completion.
|
||||||
|
key: "requestedContainers",
|
||||||
|
label: "Requested containers",
|
||||||
|
type: "number",
|
||||||
|
secondary: true,
|
||||||
|
operators: ["is", "between"],
|
||||||
|
toParams: (v) =>
|
||||||
|
v.op === "between"
|
||||||
|
? {
|
||||||
|
requestedContainersMin: v.v[0],
|
||||||
|
requestedContainersMax: v.v[1],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
requestedContainersMin: v.v[0],
|
||||||
|
requestedContainersMax: v.v[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "serviceTypeId",
|
key: "serviceTypeId",
|
||||||
label: "Service",
|
label: "Service",
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export interface BookingListFilter {
|
|||||||
/** Container count bounds — of containerTypeId when set, else of all types. */
|
/** Container count bounds — of containerTypeId when set, else of all types. */
|
||||||
containersMin?: string;
|
containersMin?: string;
|
||||||
containersMax?: string;
|
containersMax?: string;
|
||||||
|
/** Bounds on containers declared on the shipment request behind the booking. */
|
||||||
|
requestedContainersMin?: string;
|
||||||
|
requestedContainersMax?: string;
|
||||||
/** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */
|
/** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */
|
||||||
bookingType?: string;
|
bookingType?: string;
|
||||||
/** 'true' → customs bookings, 'false' → self-clearance (non-customs). */
|
/** 'true' → customs bookings, 'false' → self-clearance (non-customs). */
|
||||||
@@ -213,6 +216,10 @@ export const bookingsService = {
|
|||||||
if (filter.containerTypeId) params.containerTypeId = filter.containerTypeId;
|
if (filter.containerTypeId) params.containerTypeId = filter.containerTypeId;
|
||||||
if (filter.containersMin) params.containersMin = filter.containersMin;
|
if (filter.containersMin) params.containersMin = filter.containersMin;
|
||||||
if (filter.containersMax) params.containersMax = filter.containersMax;
|
if (filter.containersMax) params.containersMax = filter.containersMax;
|
||||||
|
if (filter.requestedContainersMin)
|
||||||
|
params.requestedContainersMin = filter.requestedContainersMin;
|
||||||
|
if (filter.requestedContainersMax)
|
||||||
|
params.requestedContainersMax = filter.requestedContainersMax;
|
||||||
if (filter.bookingType) params.bookingType = filter.bookingType;
|
if (filter.bookingType) params.bookingType = filter.bookingType;
|
||||||
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
|
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
|
||||||
if (filter.paymentCurrency)
|
if (filter.paymentCurrency)
|
||||||
@@ -257,6 +264,10 @@ export const bookingsService = {
|
|||||||
if (filter.containerTypeId) params.containerTypeId = filter.containerTypeId;
|
if (filter.containerTypeId) params.containerTypeId = filter.containerTypeId;
|
||||||
if (filter.containersMin) params.containersMin = filter.containersMin;
|
if (filter.containersMin) params.containersMin = filter.containersMin;
|
||||||
if (filter.containersMax) params.containersMax = filter.containersMax;
|
if (filter.containersMax) params.containersMax = filter.containersMax;
|
||||||
|
if (filter.requestedContainersMin)
|
||||||
|
params.requestedContainersMin = filter.requestedContainersMin;
|
||||||
|
if (filter.requestedContainersMax)
|
||||||
|
params.requestedContainersMax = filter.requestedContainersMax;
|
||||||
if (filter.bookingType) params.bookingType = filter.bookingType;
|
if (filter.bookingType) params.bookingType = filter.bookingType;
|
||||||
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
|
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
|
||||||
if (filter.paymentCurrency)
|
if (filter.paymentCurrency)
|
||||||
|
|||||||
Reference in New Issue
Block a user