feat(sync): add company details to customer addresses
Addresses with a company are completed through GET /customers_addresses/:id (tax number, company number, legal status), which the customer payload does not embed. Addresses without company get these fields set to null; a failed call keeps the stored values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common';
|
||||
import { CustomerHiboutikService } from './customer.hiboutik.service';
|
||||
|
||||
describe('CustomerHiboutikService', () => {
|
||||
const customerHiboutikRepository = { getCustomers: jest.fn(), getCustomerById: jest.fn() };
|
||||
const customerHiboutikRepository = { getCustomers: jest.fn(), getCustomerById: jest.fn(), getAddressById: jest.fn() };
|
||||
const customerDatabaseRepository = { upsert: jest.fn(), deleteMissing: jest.fn() };
|
||||
const customerConverter = { fromHiboutik: jest.fn() };
|
||||
const customerAddressDatabaseRepository = { upsert: jest.fn(), deleteMissing: jest.fn() };
|
||||
@@ -95,6 +95,47 @@ describe('CustomerHiboutikService', () => {
|
||||
expect(customerAddressDatabaseRepository.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetches the company details of addresses with a company only', async () => {
|
||||
customerHiboutikRepository.getCustomerById.mockResolvedValue({
|
||||
customers_id: 1,
|
||||
addresses: [
|
||||
{ address_id: 10, company: 'ACME' },
|
||||
{ address_id: 11, company: ' ' },
|
||||
],
|
||||
});
|
||||
customerHiboutikRepository.getAddressById.mockResolvedValue({
|
||||
address_id: 10,
|
||||
tax_number: 'FR123',
|
||||
company_number: '123',
|
||||
legal_status: 'SAS',
|
||||
updated_at: '2026-11-01 10:00:00',
|
||||
});
|
||||
|
||||
await service.syncCustomers();
|
||||
|
||||
expect(customerHiboutikRepository.getAddressById).toHaveBeenCalledTimes(1);
|
||||
expect(customerHiboutikRepository.getAddressById).toHaveBeenCalledWith(10);
|
||||
expect(customerAddressConverter.fromHiboutik).toHaveBeenCalledWith({
|
||||
address_id: 10,
|
||||
company: 'ACME',
|
||||
customers_id: 1,
|
||||
tax_number: 'FR123',
|
||||
company_number: '123',
|
||||
legal_status: 'SAS',
|
||||
updated_at: '2026-11-01 10:00:00',
|
||||
});
|
||||
});
|
||||
|
||||
it('still syncs an address whose company details cannot be fetched', async () => {
|
||||
customerHiboutikRepository.getCustomerById.mockResolvedValue({ customers_id: 1, addresses: [{ address_id: 10, company: 'ACME' }] });
|
||||
customerHiboutikRepository.getAddressById.mockRejectedValue(new Error('boom'));
|
||||
|
||||
await service.syncCustomers();
|
||||
|
||||
expect(customerAddressConverter.fromHiboutik).toHaveBeenCalledWith({ address_id: 10, company: 'ACME', customers_id: 1 });
|
||||
expect(customerAddressDatabaseRepository.upsert).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps syncing the other addresses when one fails', async () => {
|
||||
customerHiboutikRepository.getCustomerById.mockResolvedValue({
|
||||
customers_id: 1,
|
||||
|
||||
@@ -4,7 +4,11 @@ import { CustomerDatabaseRepository } from '../../database/customer.database.rep
|
||||
import { CustomerConverter } from '../../models/converter/customer.converter';
|
||||
import { CustomerAddressDatabaseRepository } from '../../database/customerAddress.database.repository';
|
||||
import { CustomerAddressConverter } from '../../models/converter/customerAddress.converter';
|
||||
import { FullCustomerHiboutikDto } from '../../models/hiboutikDTO/customer.hiboutik.dto';
|
||||
import {
|
||||
CustomerAddressDetailsHiboutikDto,
|
||||
CustomerAddressHiboutikDto,
|
||||
FullCustomerHiboutikDto,
|
||||
} from '../../models/hiboutikDTO/customer.hiboutik.dto';
|
||||
import { forEachSafely } from '../../utils/sync';
|
||||
|
||||
@Injectable()
|
||||
@@ -72,12 +76,36 @@ export class CustomerHiboutikService {
|
||||
return forEachSafely(
|
||||
hiboutikCustomer.addresses,
|
||||
// Linked to the parent customer, whatever the embedded customers_id says
|
||||
(dto) =>
|
||||
this.customerAddressDatabaseRepository.upsert(
|
||||
this.customerAddressConverter.fromHiboutik({ ...dto, customers_id: hiboutikCustomer.customers_id }),
|
||||
),
|
||||
async (dto) => {
|
||||
const address = dto.company?.trim() ? await this.withCompanyDetails(dto) : dto;
|
||||
await this.customerAddressDatabaseRepository.upsert(
|
||||
this.customerAddressConverter.fromHiboutik({ ...address, customers_id: hiboutikCustomer.customers_id }),
|
||||
);
|
||||
},
|
||||
this.logger,
|
||||
(dto) => `Hiboutik address ${dto.address_id} of customer ${hiboutikCustomer.customers_id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes an embedded address with its company details (one API call). On failure, the embedded address is returned
|
||||
* as is, so the address is still synced and its stored company details are kept.
|
||||
*/
|
||||
private async withCompanyDetails(
|
||||
dto: CustomerAddressHiboutikDto,
|
||||
): Promise<CustomerAddressHiboutikDto | CustomerAddressDetailsHiboutikDto> {
|
||||
try {
|
||||
const details = await this.customerHiboutikRepository.getAddressById(dto.address_id);
|
||||
return {
|
||||
...dto,
|
||||
tax_number: details.tax_number,
|
||||
company_number: details.company_number,
|
||||
legal_status: details.legal_status,
|
||||
updated_at: details.updated_at,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to fetch company details of Hiboutik address ${dto.address_id}: ${(error as Error)?.message}`);
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user