Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions addon/models/service-rate-fee.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro

export default class ServiceRateFeeModel extends Model {
/** @ids */
@attr('string') uuid;
@attr('string') service_rate_uuid;

/** @attributes */
Expand Down Expand Up @@ -66,6 +67,7 @@ export default class ServiceRateFeeModel extends Model {
/** @methods */
toJSON() {
return {
uuid: this.uuid,
service_rate_uuid: this.service_rate_uuid,
distance: this.distance,
distance_unit: this.distance_unit,
Expand Down
2 changes: 2 additions & 0 deletions addon/models/service-rate-parcel-fee.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { format as formatDate, isValid as isValidDate, formatDistanceToNow } fro

export default class ServiceRateParcelFeeModel extends Model {
/** @ids */
@attr('string') uuid;
@attr('string') service_rate_uuid;

/** @attributes */
Expand Down Expand Up @@ -68,6 +69,7 @@ export default class ServiceRateParcelFeeModel extends Model {
/** @methods */
toJSON() {
return {
uuid: this.uuid,
service_rate_uuid: this.service_rate_uuid,
size: this.size,
length: this.length,
Expand Down
71 changes: 66 additions & 5 deletions addon/models/service-rate.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,72 @@ export default class ServiceRate extends Model {
return this.cod_calculation_method === 'percentage';
}

@computed('rate_fees.@each.distance', 'max_distance') get rateFees() {
@computed('rate_fees.@each.{distance,min,max,unit}', 'max_distance', 'rate_calculation_method', 'isPerDrop') get rateFees() {
const existing = (this.rate_fees?.toArray?.() ?? []).filter((r) => !r.isDeleted);

if (this.isPerDrop) {
const deduped = new Map();
const rankFee = (fee) => {
if (fee.id && !fee.isNew) {
return 3;
}

if (!fee.isNew) {
return 2;
}

return 1;
};

existing
.filter((r) => r.unit === 'waypoint')
.forEach((fee) => {
const key = `drop:${fee.min}:${fee.max}:${fee.unit}`;
const current = deduped.get(key);

if (!current || rankFee(fee) >= rankFee(current)) {
deduped.set(key, fee);
}
});

return Array.from(deduped.values()).sort((a, b) => (a.min ?? 0) - (b.min ?? 0));
}

const n = Math.max(0, Number(this.max_distance) || 0);
const existing = (this.rate_fees?.toArray?.() ?? []).filter((r) => r.distance !== null && r.distance !== undefined && !r.isDeleted);

// Return existing fees sorted by distance, filtered by max_distance
return existing.filter((r) => r.distance >= 0 && r.distance < n).sort((a, b) => a.distance - b.distance);
return existing.filter((r) => r.distance !== null && r.distance !== undefined && r.distance >= 0 && r.distance < n).sort((a, b) => a.distance - b.distance);
}

@computed('parcel_fees.@each.{size,length,width,height,dimensions_unit,weight,weight_unit,fee,id}') get parcelFees() {
const existing = (this.parcel_fees?.toArray?.() ?? []).filter((fee) => !fee.isDeleted);
const deduped = new Map();

const feeKey = (fee) => {
return [fee.size, fee.length, fee.width, fee.height, fee.dimensions_unit, fee.weight, fee.weight_unit].join(':');
};

const rankFee = (fee) => {
if (fee.id && !fee.isNew) {
return 3;
}

if (!fee.isNew) {
return 2;
}

return 1;
};

existing.forEach((fee) => {
const key = feeKey(fee);
const current = deduped.get(key);

if (!current || rankFee(fee) >= rankFee(current)) {
deduped.set(key, fee);
}
});

return Array.from(deduped.values());
}

/** @methods */
Expand All @@ -141,7 +201,8 @@ export default class ServiceRate extends Model {
const store = getOwner(this).lookup('service:store');
const existingFees = this.rate_fees?.toArray?.() ?? [];
const last = existingFees[existingFees.length - 1];
const min = last ? last.max + 1 : 1;
const lastMax = Number(last?.max) || 0;
const min = last ? lastMax + 1 : 1;
const max = min + 5;

const newFee = store.createRecord('service-rate-fee', {
Expand Down
18 changes: 15 additions & 3 deletions addon/serializers/service-rate.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,24 @@ export default class ServiceRateSerializer extends ApplicationSerializer.extend(
const savedRateFees = allRateFees.filter((f) => !f.isNew);
const unsavedRateFees = allRateFees.filter((f) => f.isNew);

// Create a map of saved fees by distance
const savedByDistance = new Map(savedRateFees.map((f) => [f.distance, f]));
// Create a map of saved fees using the most stable key for the fee shape.
const savedFeeKey = (fee) => {
if (fee.id) {
return `id:${fee.id}`;
}

if (fee.unit === 'waypoint') {
return `drop:${fee.min}:${fee.max}:${fee.unit}`;
}

return `distance:${fee.distance}`;
};

const savedByKey = new Map(savedRateFees.map((f) => [savedFeeKey(f), f]));

// Only remove unsaved fees that duplicate saved fees
unsavedRateFees.forEach((fee) => {
if (savedByDistance.has(fee.distance)) {
if (savedByKey.has(savedFeeKey(fee))) {
serviceRate.get('rate_fees').removeObject(fee);
fee.unloadRecord();
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fleetbase/fleetops-data",
"version": "0.1.29",
"version": "0.1.30",
"description": "Fleetbase Fleet-Ops based models, serializers, transforms, adapters and GeoJson utility functions.",
"keywords": [
"fleetbase-data",
Expand Down
176 changes: 175 additions & 1 deletion tests/unit/models/service-rate-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,184 @@ import { setupTest } from 'dummy/tests/helpers';
module('Unit | Model | service rate', function (hooks) {
setupTest(hooks);

// Replace this with your real tests.
test('it exists', function (assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('service-rate', {});
assert.ok(model);
});

test('rateFees returns per-drop fees sorted by min when using per_drop', function (assert) {
const store = this.owner.lookup('service:store');
const serviceRate = store.createRecord('service-rate', {
rate_calculation_method: 'per_drop',
});

serviceRate.rate_fees.pushObjects([
store.createRecord('service-rate-fee', { min: 6, max: 10, unit: 'waypoint', fee: 200 }),
store.createRecord('service-rate-fee', { min: 1, max: 5, unit: 'waypoint', fee: 100 }),
store.createRecord('service-rate-fee', { distance: 0, fee: 50 }),
]);

assert.deepEqual(
serviceRate.rateFees.map((fee) => [fee.min, fee.max]),
[
[1, 5],
[6, 10],
]
);
});

test('rateFees filters fixed-distance fees by max_distance', function (assert) {
const store = this.owner.lookup('service:store');
const serviceRate = store.createRecord('service-rate', {
rate_calculation_method: 'fixed_rate',
max_distance: 2,
});

serviceRate.rate_fees.pushObjects([
store.createRecord('service-rate-fee', { distance: 0, fee: 100 }),
store.createRecord('service-rate-fee', { distance: 1, fee: 200 }),
store.createRecord('service-rate-fee', { distance: 2, fee: 300 }),
]);

assert.deepEqual(
serviceRate.rateFees.map((fee) => fee.distance),
[0, 1]
);
});

test('parcelFees prefers persisted parcel fees over duplicate unsaved defaults', function (assert) {
const store = this.owner.lookup('service:store');
const serviceRate = store.createRecord('service-rate', {
rate_calculation_method: 'parcel',
});

const unsavedDefault = store.createRecord('service-rate-parcel-fee', {
size: 'small',
length: 34,
width: 18,
height: 10,
dimensions_unit: 'cm',
weight: 2,
weight_unit: 'kg',
fee: 0,
});

const persistedFee = store.push({
data: {
type: 'service-rate-parcel-fee',
id: 'parcel-fee-1',
attributes: {
size: 'small',
length: '34',
width: '18',
height: '10',
dimensions_unit: 'cm',
weight: '2',
weight_unit: 'kg',
fee: '5',
},
},
});

serviceRate.parcel_fees.pushObjects([unsavedDefault, persistedFee]);

assert.strictEqual(serviceRate.parcelFees.length, 1);
assert.strictEqual(serviceRate.parcelFees[0].id, 'parcel-fee-1');
assert.strictEqual(serviceRate.parcelFees[0].fee, '5');
});

test('parcelFees prefers the latest duplicate persisted parcel fee in store state', function (assert) {
const store = this.owner.lookup('service:store');
const serviceRate = store.createRecord('service-rate', {
rate_calculation_method: 'parcel',
});

const staleFee = store.createRecord('service-rate-parcel-fee', {
size: 'small',
length: 34,
width: 18,
height: 10,
dimensions_unit: 'cm',
weight: 2,
weight_unit: 'kg',
fee: '0',
});
staleFee.set('id', 'parcel-fee-stale');

const updatedFee = store.createRecord('service-rate-parcel-fee', {
size: 'small',
length: 34,
width: 18,
height: 10,
dimensions_unit: 'cm',
weight: 2,
weight_unit: 'kg',
fee: '12',
});
updatedFee.set('id', 'parcel-fee-updated');

serviceRate.parcel_fees.pushObjects([staleFee, updatedFee]);

assert.strictEqual(serviceRate.parcelFees.length, 1);
assert.strictEqual(serviceRate.parcelFees[0].id, 'parcel-fee-updated');
assert.strictEqual(serviceRate.parcelFees[0].fee, '12');
});

test('addPerDropRateFee increments numeric ranges even when existing values are strings', function (assert) {
const store = this.owner.lookup('service:store');
const serviceRate = store.createRecord('service-rate', {
rate_calculation_method: 'per_drop',
currency: 'USD',
});

serviceRate.rate_fees.pushObject(
store.createRecord('service-rate-fee', {
min: '1',
max: '2',
unit: 'waypoint',
fee: 100,
})
);

serviceRate.addPerDropRateFee();

const addedFee = serviceRate.rate_fees[1];

assert.strictEqual(addedFee.min, 3);
assert.strictEqual(addedFee.max, 8);
});

test('rateFees prefers persisted per-drop fees over duplicate unsaved rows', function (assert) {
const store = this.owner.lookup('service:store');
const serviceRate = store.createRecord('service-rate', {
rate_calculation_method: 'per_drop',
});

const unsavedDefault = store.createRecord('service-rate-fee', {
min: 1,
max: 5,
unit: 'waypoint',
fee: 0,
});

const persistedFee = store.push({
data: {
type: 'service-rate-fee',
id: 'rate-fee-1',
attributes: {
min: 1,
max: 5,
unit: 'waypoint',
fee: '5',
},
},
});

serviceRate.rate_fees.pushObjects([unsavedDefault, persistedFee]);

assert.strictEqual(serviceRate.rateFees.length, 1);
assert.strictEqual(serviceRate.rateFees[0].id, 'rate-fee-1');
assert.strictEqual(serviceRate.rateFees[0].fee, '5');
});
});
Loading