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
9 changes: 9 additions & 0 deletions addon/models/fuel-provider-connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class FuelProviderConnectionModel extends Model {
@attr('string') name;
@attr('string', { defaultValue: 'production' }) environment;
@attr('string', { defaultValue: 'configured' }) status;
@attr('raw') credentials;
@attr('raw') sync_settings;
@attr('raw') last_sync_state;
@attr('string') last_error;
Expand All @@ -25,6 +26,14 @@ export default class FuelProviderConnectionModel extends Model {
return this.name || this.provider;
}

@computed('last_sync_state.summary.imported') get lastImported() {
return String(this.last_sync_state?.summary?.imported ?? 0);
}

@computed('last_sync_state.summary.unmatched') get lastUnmatched() {
return String(this.last_sync_state?.summary?.unmatched ?? 0);
}

@computed('last_synced_at') get lastSyncedAt() {
return this.formatDate(this.last_synced_at);
}
Expand Down
23 changes: 23 additions & 0 deletions addon/models/fuel-provider-sync-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Model, { attr } from '@ember-data/model';

export default class FuelProviderSyncRunModel extends Model {
@attr('string') public_id;
@attr('string') fuel_provider_connection_uuid;
@attr('string') provider;
@attr('string') status;
@attr('string') error;
@attr('number') imported;
@attr('number') matched;
@attr('number') unmatched;
@attr('number') fuel_reports_created;
@attr('number') liters;
@attr('number') amount;
@attr('date') from;
@attr('date') to;
@attr('date') started_at;
@attr('date') finished_at;
@attr('date') created_at;
@attr('date') updated_at;
@attr('raw') summary;
@attr('raw') meta;
}
14 changes: 13 additions & 1 deletion addon/serializers/fuel-provider-connection.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';

export default class FuelProviderConnectionSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {}
export default class FuelProviderConnectionSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
serialize() {
const json = super.serialize(...arguments);

// Credentials are write-only. A fetched record has no credentials to send
// until the operator supplies a replacement.
if (json.credentials === null || json.credentials === undefined) {
delete json.credentials;
}

return json;
}
}
3 changes: 3 additions & 0 deletions addon/serializers/fuel-provider-sync-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';

export default class FuelProviderSyncRunSerializer extends ApplicationSerializer {}
1 change: 1 addition & 0 deletions app/models/fuel-provider-sync-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/fleetops-data/models/fuel-provider-sync-run';
1 change: 1 addition & 0 deletions app/serializers/fuel-provider-sync-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/fleetops-data/serializers/fuel-provider-sync-run';
32 changes: 32 additions & 0 deletions tests/unit/models/fuel-provider-connection-test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { module, test } from 'qunit';
import { set } from '@ember/object';
import { setupTest } from 'dummy/tests/helpers';
import { FIXED_DATE, FIXED_DATE_LONG, THREE_DAYS_DISTANCE, assertDateGetters, assertDefaults, assertRelationships } from 'dummy/tests/helpers/model-contract';

Expand Down Expand Up @@ -35,6 +36,37 @@ module('Unit | Model | fuel provider connection', function (hooks) {
});

module('display and formatting', function () {
test('sync counts default to zero when a connection has no complete summary', function (assert) {
const connection = this.store.createRecord('fuel-provider-connection');

assert.strictEqual(connection.lastImported, '0', 'a new connection has no imports');
assert.strictEqual(connection.lastUnmatched, '0', 'a new connection has no unmatched purchases');

for (const state of [null, {}, { summary: null }, { summary: {} }, { summary: { imported: null, unmatched: null } }]) {
connection.set('last_sync_state', state);
assert.strictEqual(connection.lastImported, '0', 'an absent import count has a readable fallback');
assert.strictEqual(connection.lastUnmatched, '0', 'an absent unmatched count has a readable fallback');
}
});

test('sync counts display the summary and update when a later sync changes it', function (assert) {
const connection = this.store.createRecord('fuel-provider-connection', {
last_sync_state: { summary: { imported: 12, unmatched: 3 } },
});

assert.strictEqual(connection.lastImported, '12');
assert.strictEqual(connection.lastUnmatched, '3');

connection.set('last_sync_state', { summary: { imported: 5, unmatched: 0 } });
assert.strictEqual(connection.lastImported, '5', 'replacing the sync result invalidates the cached count');
assert.strictEqual(connection.lastUnmatched, '0', 'a successful zero count is preserved');

set(connection, 'last_sync_state.summary.imported', 8);
set(connection, 'last_sync_state.summary.unmatched', 2);
assert.strictEqual(connection.lastImported, '8', 'nested import updates invalidate the cached count');
assert.strictEqual(connection.lastUnmatched, '2', 'nested unmatched updates invalidate the cached count');
});

test('displayName prefers the connection name over the provider', function (assert) {
const connection = this.store.createRecord('fuel-provider-connection', { name: 'Shell APAC', provider: 'shell' });
assert.strictEqual(connection.displayName, 'Shell APAC');
Expand Down
70 changes: 70 additions & 0 deletions tests/unit/serializers/fuel-provider-connection-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { module, test } from 'qunit';
import { setupTest } from 'dummy/tests/helpers';
import RESTAdapter from '@ember-data/adapter/rest';
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
import { assertEmbeddedAttrs, assertNormalizesUuidAsId, assertPrimaryKeyIsUuid } from 'dummy/tests/helpers/serializer-contract';
Expand Down Expand Up @@ -33,4 +34,73 @@ module('Unit | Serializer | fuel provider connection', function (hooks) {

assert.strictEqual(record.serialize().provider, 'contract-value');
});

test('the token used for connection testing is included in the create payload', function (assert) {
const credentials = { api_token: 'test-token', auth_type: 'ws_sk_header' };
const record = this.store.createRecord('fuel-provider-connection', { provider: 'petroapp', environment: 'sandbox', credentials });
const payload = record.serialize();

assert.deepEqual(payload.credentials, credentials, 'Ember Data serializes the token and authentication method');
assert.strictEqual(payload.environment, 'sandbox');
record.set('credentials', { api_token: 'replacement-token' });
assert.deepEqual(record.serialize().credentials, { api_token: 'replacement-token' }, 'credential edits are tracked and serialized');
});

test('editing a fetched connection omits credentials until a replacement is supplied', function (assert) {
const record = assertNormalizesUuidAsId(assert, this.store, 'fuel-provider-connection', { provider: 'petroapp', name: 'Sandbox' });
record.set('name', 'Renamed sandbox');

assert.false(Object.hasOwn(record.serialize(), 'credentials'), 'a name-only update cannot clear the stored token');
record.set('credentials', null);
assert.false(Object.hasOwn(record.serialize(), 'credentials'), 'null credentials are also omitted');
record.set('credentials', { api_token: 'replacement-token' });
assert.deepEqual(record.serialize().credentials, { api_token: 'replacement-token' });
});

test('saving a connection sends credentials and accepts the write-only server response', async function (assert) {
const credentials = { api_token: 'test-token', auth_type: 'ws_sk_header' };
this.owner.register(
'adapter:fuel-provider-connection',
class extends RESTAdapter {
ajax(url, method, options) {
assert.strictEqual(method, 'POST');
assert.deepEqual(options.data.fuelProviderConnection.credentials, credentials);
assert.strictEqual(options.data.fuelProviderConnection.environment, 'sandbox');

return Promise.resolve({
fuelProviderConnection: {
uuid: 'saved-connection',
provider: 'petroapp',
name: 'Sandbox',
environment: 'sandbox',
status: 'configured',
},
});
}
}
);
const record = this.store.createRecord('fuel-provider-connection', { provider: 'petroapp', name: 'Sandbox', environment: 'sandbox', credentials });
await record.save();
assert.strictEqual(record.id, 'saved-connection');
assert.false(record.isNew, 'the save completes without requiring credentials in the response');
});

test('repeated public-ID lookups reuse the UUID record without an identifier collision', async function (assert) {
let requests = 0;
this.owner.register(
'adapter:fuel-provider-connection',
class extends RESTAdapter {
queryRecord(store, type, query) {
requests++;
assert.deepEqual(query, { public_id: 'fuel_provider_connection_test', single: true });
return Promise.resolve({ fuelProviderConnection: { uuid: 'connection-uuid', public_id: 'fuel_provider_connection_test', provider: 'petroapp', status: 'connected' } });
}
}
);
const first = await this.store.queryRecord('fuel-provider-connection', { public_id: 'fuel_provider_connection_test', single: true });
const second = await this.store.queryRecord('fuel-provider-connection', { public_id: 'fuel_provider_connection_test', single: true });
assert.strictEqual(first.id, 'connection-uuid');
assert.strictEqual(second, first, 'refreshing the public URL keeps one record with its canonical UUID');
assert.strictEqual(requests, 2);
});
});
48 changes: 48 additions & 0 deletions tests/unit/serializers/fuel-provider-sync-run-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { module, test } from 'qunit';
import { setupTest } from 'dummy/tests/helpers';
import RESTAdapter from '@ember-data/adapter/rest';

module('Unit | Serializer | fuel provider sync run', function (hooks) {
setupTest(hooks);

test('querying sync history resolves the registered model and server payload', async function (assert) {
this.owner.register(
'adapter:fuel-provider-sync-run',
class extends RESTAdapter {
ajax(url, method, options) {
assert.strictEqual(method, 'GET');
assert.deepEqual(options.data, { connection: 'connection-123', sort: '-created_at' });
return Promise.resolve({
fuelProviderSyncRuns: [
{
uuid: 'run-123',
fuel_provider_connection_uuid: 'connection-123',
provider: 'petroapp',
status: 'completed',
from: '2026-09-01T00:00:00Z',
to: '2026-09-15T00:00:00Z',
finished_at: '2026-09-15T01:00:00Z',
imported: 12,
matched: 10,
unmatched: 2,
summary: { imported: 12 },
meta: {},
},
],
});
}
}
);
const store = this.owner.lookup('service:store');
const runs = await store.query('fuel-provider-sync-run', { connection: 'connection-123', sort: '-created_at' });
const run = runs.objectAt(0);
assert.strictEqual(runs.length, 1);
assert.strictEqual(run.id, 'run-123', 'the internal UUID is the record identity');
assert.strictEqual(run.status, 'completed');
assert.strictEqual(run.imported, 12);
assert.strictEqual(run.matched, 10);
assert.strictEqual(run.from.toISOString(), '2026-09-01T00:00:00.000Z');
assert.strictEqual(run.finished_at.toISOString(), '2026-09-15T01:00:00.000Z');
assert.deepEqual(run.summary, { imported: 12 });
});
});
Loading