Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db1b9a2c96 |
@@ -137,6 +137,10 @@ class customer_mass_import_service
|
||||
if ($cvrLength < 8 || $cvrLength > 20) {
|
||||
throw new \RuntimeException('CVR must be between 8 and 20 digits.', 400);
|
||||
}
|
||||
|
||||
if ($normalized['ean'] !== null && strlen((string)$normalized['ean']) > 13) {
|
||||
throw new \RuntimeException('EAN must be at most 13 digits.', 400);
|
||||
}
|
||||
}
|
||||
|
||||
protected function normalizePositiveInt(mixed $value): ?int
|
||||
|
||||
@@ -172,7 +172,8 @@ class economic implements economic_i
|
||||
string $email,
|
||||
int $phone,
|
||||
?int $mobile_phone = null,
|
||||
object|array|null $company_information = null
|
||||
object|array|null $company_information = null,
|
||||
?string $ean = null
|
||||
): object
|
||||
{
|
||||
$payload = [
|
||||
@@ -196,10 +197,37 @@ class economic implements economic_i
|
||||
];
|
||||
|
||||
$payload = array_replace($payload, $this->buildCustomerPayloadFromCompanyInformation($company_information));
|
||||
$normalized_ean = self::normalizeCustomerEan($ean);
|
||||
if ($normalized_ean !== null) {
|
||||
$payload['ean'] = $normalized_ean;
|
||||
}
|
||||
|
||||
return $this->customers->customers->create($payload);
|
||||
}
|
||||
|
||||
public static function normalizeCustomerEan(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$digits = preg_replace('/\D+/', '', (string)$value);
|
||||
if (!is_string($digits)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$digits = trim($digits);
|
||||
if ($digits === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strlen($digits) > 13) {
|
||||
throw new \InvalidArgumentException('EAN must be at most 13 digits.');
|
||||
}
|
||||
|
||||
return $digits;
|
||||
}
|
||||
|
||||
private function buildCustomerPayloadFromCompanyInformation(object|array|null $company_information): array
|
||||
{
|
||||
if ($company_information === null) {
|
||||
|
||||
@@ -240,13 +240,7 @@ class economic_transfer_executor
|
||||
'Queued transfer processed successfully for collected invoice #' . $collected_invoice_id
|
||||
);
|
||||
|
||||
$result = $collected_order_invoices->asArray();
|
||||
$transfer_metrics = $collected_order_invoices->getLastEconomicTransferMetrics();
|
||||
if ($transfer_metrics !== null) {
|
||||
$result['economic_transfer_metrics'] = $transfer_metrics;
|
||||
}
|
||||
|
||||
return $result;
|
||||
return $collected_order_invoices->asArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -112,134 +112,6 @@ class limited_backoffice_service
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, array{group:string,capability:string}>
|
||||
*/
|
||||
private const ROLE_PERMISSION_CAPABILITIES = [
|
||||
'user' => [
|
||||
'group' => 'account',
|
||||
'capability' => 'sign_in',
|
||||
],
|
||||
'permissions_list_own' => [
|
||||
'group' => 'account',
|
||||
'capability' => 'view_own_permissions',
|
||||
],
|
||||
'list_orders' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'view_orders',
|
||||
],
|
||||
'add_order' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'create_orders',
|
||||
],
|
||||
'edit_order' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'edit_orders',
|
||||
],
|
||||
'delete_order' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'delete_orders',
|
||||
],
|
||||
'list_order_items' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'view_order_items',
|
||||
],
|
||||
'add_order_items' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'create_order_items',
|
||||
],
|
||||
'edit_order_items' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'update_order_lines',
|
||||
],
|
||||
'delete_order_items' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'remove_order_lines',
|
||||
],
|
||||
'charge_order' => [
|
||||
'group' => 'orders',
|
||||
'capability' => 'charge_orders',
|
||||
],
|
||||
'list_bookings' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'view_department_bookings',
|
||||
],
|
||||
'list_own_bookings' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'view_own_bookings',
|
||||
],
|
||||
'edit_bookings' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'update_bookings',
|
||||
],
|
||||
'add_booking' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'create_bookings',
|
||||
],
|
||||
'complete_bookings' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'mark_bookings_complete',
|
||||
],
|
||||
'resend_booking_confirmations' => [
|
||||
'group' => 'bookings',
|
||||
'capability' => 'send_booking_confirmations',
|
||||
],
|
||||
'department_timebookings_entries_get' => [
|
||||
'group' => 'time_bookings',
|
||||
'capability' => 'view_time_booking_entries',
|
||||
],
|
||||
'department_timebookings_entries_post' => [
|
||||
'group' => 'time_bookings',
|
||||
'capability' => 'create_time_booking_entries',
|
||||
],
|
||||
'department_timebookings_entries_put' => [
|
||||
'group' => 'time_bookings',
|
||||
'capability' => 'edit_time_booking_entries',
|
||||
],
|
||||
'statistics_orders_new' => [
|
||||
'group' => 'reports',
|
||||
'capability' => 'view_order_statistics',
|
||||
],
|
||||
'statistics_bookings_new' => [
|
||||
'group' => 'reports',
|
||||
'capability' => 'view_booking_statistics',
|
||||
],
|
||||
self::PERMISSION_ACCESS => [
|
||||
'group' => 'limited_backoffice',
|
||||
'capability' => 'open_limited_backoffice',
|
||||
],
|
||||
self::PERMISSION_MANAGE_PRICES => [
|
||||
'group' => 'limited_backoffice',
|
||||
'capability' => 'manage_department_prices',
|
||||
],
|
||||
self::PERMISSION_MANAGE_EMPLOYEES => [
|
||||
'group' => 'limited_backoffice',
|
||||
'capability' => 'manage_employee_access',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private const ROLE_PERMISSION_GROUP_ORDER = [
|
||||
'account',
|
||||
'orders',
|
||||
'bookings',
|
||||
'time_bookings',
|
||||
'reports',
|
||||
'limited_backoffice',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<int, true>
|
||||
*/
|
||||
private const PHONE_COUNTRY_CODES = [
|
||||
45 => true,
|
||||
46 => true,
|
||||
47 => true,
|
||||
358 => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
@@ -251,7 +123,7 @@ class limited_backoffice_service
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{key:string,label:string,description:string,permission_groups:array<int,array{key:string,capabilities:array<int,string>}>}>
|
||||
* @return array<int, array{key:string,label:string,description:string}>
|
||||
*/
|
||||
public function rolePresets(): array
|
||||
{
|
||||
@@ -261,45 +133,11 @@ class limited_backoffice_service
|
||||
'key' => $key,
|
||||
'label' => $preset['label'],
|
||||
'description' => $preset['description'],
|
||||
'permission_groups' => $this->rolePermissionGroups($preset['permissions']),
|
||||
];
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $permissions
|
||||
* @return array<int, array{key:string,capabilities:array<int,string>}>
|
||||
*/
|
||||
private function rolePermissionGroups(array $permissions): array
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($permissions as $permission) {
|
||||
$capability = self::ROLE_PERMISSION_CAPABILITIES[$permission] ?? null;
|
||||
if ($capability === null) {
|
||||
throw new \RuntimeException('Missing limited backoffice role capability for permission: ' . $permission);
|
||||
}
|
||||
|
||||
$group = $capability['group'];
|
||||
$groups[$group] ??= [];
|
||||
$groups[$group][] = $capability['capability'];
|
||||
}
|
||||
|
||||
$payload = [];
|
||||
foreach (self::ROLE_PERMISSION_GROUP_ORDER as $group) {
|
||||
if (!isset($groups[$group])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$payload[] = [
|
||||
'key' => $group,
|
||||
'capabilities' => array_values(array_unique($groups[$group])),
|
||||
];
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
@@ -544,8 +382,7 @@ class limited_backoffice_service
|
||||
$roleKey = $this->normalizeRoleKey($payload['role_key'] ?? null);
|
||||
$displayName = $this->normalizeRequiredString($payload['display_name'] ?? null, 'Display name is required.');
|
||||
$password = $this->normalizePassword($payload['password'] ?? null, true);
|
||||
$email = $this->normalizeEmail($payload['email'] ?? null, true);
|
||||
$phone = $this->normalizeOptionalPhonePair($payload);
|
||||
$email = $this->normalizeOptionalString($payload['email'] ?? null);
|
||||
|
||||
$mysqli = $this->mysqli();
|
||||
$mysqli->begin_transaction();
|
||||
@@ -556,23 +393,13 @@ class limited_backoffice_service
|
||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
$statement = $mysqli->prepare(
|
||||
'INSERT INTO `users`
|
||||
(`customer_number`, `display_name`, `email`, `password`, `group_id`, `phone_country_code`, `phone`)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
'INSERT INTO `users` (`customer_number`, `display_name`, `email`, `password`, `group_id`)
|
||||
VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
if ($statement === false) {
|
||||
throw new \RuntimeException('Unable to prepare employee insert.');
|
||||
}
|
||||
$statement->bind_param(
|
||||
'isssiii',
|
||||
$customerNumber,
|
||||
$displayName,
|
||||
$email,
|
||||
$passwordHash,
|
||||
$groupId,
|
||||
$phone['phone_country_code'],
|
||||
$phone['phone']
|
||||
);
|
||||
$statement->bind_param('isssi', $customerNumber, $displayName, $email, $passwordHash, $groupId);
|
||||
$statement->execute();
|
||||
$employeeId = (int)$mysqli->insert_id;
|
||||
$statement->close();
|
||||
@@ -650,12 +477,11 @@ class limited_backoffice_service
|
||||
? $this->normalizeRequiredString($payload['display_name'], 'Display name is required.')
|
||||
: null;
|
||||
$email = array_key_exists('email', $payload)
|
||||
? $this->normalizeEmail($payload['email'], true)
|
||||
? $this->normalizeOptionalString($payload['email'])
|
||||
: null;
|
||||
$password = array_key_exists('password', $payload)
|
||||
? $this->normalizePassword($payload['password'], false)
|
||||
: null;
|
||||
$phone = $this->normalizeOptionalPhonePair($payload, false);
|
||||
$active = array_key_exists('active', $payload)
|
||||
? (bool)$payload['active']
|
||||
: $this->isEmployeeRowActive($employee);
|
||||
@@ -685,10 +511,6 @@ class limited_backoffice_service
|
||||
if ($password !== null) {
|
||||
$userUpdates['password'] = password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
if ($phone !== null) {
|
||||
$userUpdates['phone_country_code'] = $phone['phone_country_code'];
|
||||
$userUpdates['phone'] = $phone['phone'];
|
||||
}
|
||||
$usersHaveDeletedAt = $this->tableHasColumn('users', 'deleted_at');
|
||||
if ($active) {
|
||||
$userUpdates['group_id'] = $managedGroupId;
|
||||
@@ -1087,89 +909,6 @@ class limited_backoffice_service
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
private function normalizeEmail(mixed $value, bool $required): ?string
|
||||
{
|
||||
$email = $this->normalizeOptionalString($value);
|
||||
if ($email === null) {
|
||||
if ($required) {
|
||||
throw new limited_backoffice_exception('Email is required.', 400);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
|
||||
throw new limited_backoffice_exception('Email must be a valid email address.', 400);
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array{phone_country_code:int|null,phone:int|null}|null
|
||||
*/
|
||||
private function normalizeOptionalPhonePair(array $payload, bool $defaultWhenMissing = true): ?array
|
||||
{
|
||||
$hasCountryCode = array_key_exists('phone_country_code', $payload);
|
||||
$hasPhone = array_key_exists('phone', $payload);
|
||||
if (!$hasCountryCode && !$hasPhone) {
|
||||
return $defaultWhenMissing
|
||||
? ['phone_country_code' => null, 'phone' => null]
|
||||
: null;
|
||||
}
|
||||
|
||||
if (!$hasCountryCode || !$hasPhone) {
|
||||
throw new limited_backoffice_exception('Phone country code and phone number must be provided together.', 400);
|
||||
}
|
||||
|
||||
$countryCode = $this->normalizeOptionalDigits($payload['phone_country_code']);
|
||||
$phone = $this->normalizeOptionalDigits($payload['phone']);
|
||||
if ($countryCode === null && $phone === null) {
|
||||
return ['phone_country_code' => null, 'phone' => null];
|
||||
}
|
||||
|
||||
if ($countryCode === null || $phone === null) {
|
||||
throw new limited_backoffice_exception('Phone country code and phone number must be provided together.', 400);
|
||||
}
|
||||
|
||||
if (!isset(self::PHONE_COUNTRY_CODES[$countryCode])) {
|
||||
throw new limited_backoffice_exception('Phone country code is not supported.', 400);
|
||||
}
|
||||
|
||||
$phoneText = (string)$phone;
|
||||
if (!preg_match('/^\d{4,15}$/', $phoneText)) {
|
||||
throw new limited_backoffice_exception('Phone number must be 4-15 digits.', 400);
|
||||
}
|
||||
|
||||
return [
|
||||
'phone_country_code' => $countryCode,
|
||||
'phone' => $phone,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeOptionalDigits(mixed $value): ?int
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_int($value)) {
|
||||
return $value > 0 ? $value : null;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
if (ctype_digit($value)) {
|
||||
return (int)$value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new limited_backoffice_exception('Phone values must contain digits only.', 400);
|
||||
}
|
||||
|
||||
private function normalizePassword(mixed $value, bool $required): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
@@ -1345,8 +1084,6 @@ class limited_backoffice_service
|
||||
'customer_number' => (int)$row['customer_number'],
|
||||
'display_name' => (string)($row['display_name'] ?? ''),
|
||||
'email' => $row['email'] === null ? null : (string)$row['email'],
|
||||
'phone_country_code' => $row['phone_country_code'] === null ? null : (int)$row['phone_country_code'],
|
||||
'phone' => $row['phone'] === null ? null : (int)$row['phone'],
|
||||
'active' => $active,
|
||||
'role' => $this->rolePayload((string)$row['role_key']),
|
||||
'departments' => $this->departmentSummaries($departmentIds),
|
||||
@@ -1467,7 +1204,7 @@ class limited_backoffice_service
|
||||
$types = '';
|
||||
$values = [];
|
||||
foreach ($fields as $field => $value) {
|
||||
if (!in_array($field, ['display_name', 'email', 'password', 'group_id', 'deleted_at', 'phone_country_code', 'phone'], true)) {
|
||||
if (!in_array($field, ['display_name', 'email', 'password', 'group_id', 'deleted_at'], true)) {
|
||||
continue;
|
||||
}
|
||||
if ($value === null) {
|
||||
|
||||
@@ -14,6 +14,8 @@ class economic_customer_mo
|
||||
public null|string $message;
|
||||
public null|string $corporateIdentificationNumber;
|
||||
public null|string $email;
|
||||
public null|string $ean;
|
||||
public null|string $publicEntryNumber;
|
||||
public null|string $mobilePhone;
|
||||
public null|string $currency;
|
||||
public null|string $country;
|
||||
@@ -46,6 +48,8 @@ class economic_customer_mo
|
||||
$this->zip = ($customer->zip ?? null);
|
||||
$this->corporateIdentificationNumber = ($customer->corporateIdentificationNumber ?? null);
|
||||
$this->email = ($customer->email ?? null);
|
||||
$this->ean = ($customer->ean ?? null);
|
||||
$this->publicEntryNumber = ($customer->publicEntryNumber ?? $customer->public_entry_number ?? null);
|
||||
$this->mobilePhone = ($customer->mobilePhone ?? null);
|
||||
$this->currency = ($customer->currency ?? null);
|
||||
$this->country = ($customer->country ?? null);
|
||||
@@ -100,6 +104,8 @@ class economic_customer_mo
|
||||
'zip' => $this->zip,
|
||||
'corporateIdentificationNumber' => $this->corporateIdentificationNumber,
|
||||
'email' => $this->email,
|
||||
'ean' => $this->ean,
|
||||
'publicEntryNumber' => $this->publicEntryNumber,
|
||||
'mobilePhone' => $this->mobilePhone,
|
||||
'currency' => $this->currency,
|
||||
'country' => $this->country,
|
||||
|
||||
+5
-33
@@ -61,47 +61,19 @@ class economic_invoices_draft_endpoint
|
||||
* @throws Exception If the request fails
|
||||
*/
|
||||
public function add_order(int $invoiceDraftId, orders_o $order, string $currency = 'DKK'): void
|
||||
{
|
||||
$this->add_orders($invoiceDraftId, [$order], $currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add many orders to a draft invoice and flush their lines in batches.
|
||||
*
|
||||
* @param orders_o[] $orders
|
||||
* @return array{order_count:int,orders_with_invoice_lines:int,line_count:int,batch_count:int,batch_sizes:array<int,int>}
|
||||
* @throws Exception If the request fails
|
||||
*/
|
||||
public function add_orders(int $invoiceDraftId, array $orders, string $currency = 'DKK', int $line_batch_size = 500): array
|
||||
{
|
||||
$draftInvoice = (new economic())->getInvoiceDraft($invoiceDraftId, strtoupper($currency), true);
|
||||
$orders_with_invoice_lines = 0;
|
||||
|
||||
foreach ( $orders as $order ) {
|
||||
if (!$order instanceof orders_o) {
|
||||
throw new Exception('Order payload must contain orders_o instances');
|
||||
}
|
||||
// Check if the order includes any items that should be included in the invoice
|
||||
if ($order->getIncludeInInvoiceCount() <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$orders_with_invoice_lines++;
|
||||
// Check if the order includes any items that should be included in the invoice
|
||||
if ($order->getIncludeInInvoiceCount() > 0) {
|
||||
// Add the transaction header (Timestamp, department, etc.)
|
||||
$draftInvoice->addNewTransactionHeader($order);
|
||||
// Add the order lines
|
||||
$draftInvoice->addOrderItemLines($order);
|
||||
// Add an empty line, so the invoice is not empty
|
||||
$draftInvoice->addTextLine('');
|
||||
// Save the draft invoice lines
|
||||
$draftInvoice->addLines();
|
||||
}
|
||||
|
||||
$metrics = $draftInvoice->flushLinesInBatches($line_batch_size);
|
||||
|
||||
return [
|
||||
'order_count' => count($orders),
|
||||
'orders_with_invoice_lines' => $orders_with_invoice_lines,
|
||||
...$metrics,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,4 +151,4 @@ class economic_invoices_draft_endpoint
|
||||
$draft_invoice->addLines();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-10
@@ -127,6 +127,23 @@ class economic_invoices_drafts_endpoint
|
||||
$customer_address = $customer->getAddress() ?? 'Ukendt';
|
||||
$customer_zip = $customer->getZipCode() ?? 'Ukendt';
|
||||
$customer_city = $customer->getCity() ?? 'Ukendt';
|
||||
$recipient = [
|
||||
'name' => $customer_name,
|
||||
'address' => $customer_address,
|
||||
'zip' => $customer_zip,
|
||||
'city' => $customer_city,
|
||||
'vatZone' => [
|
||||
'vatZoneNumber' => (int)$customer->getVatZoneNumber(),
|
||||
],
|
||||
];
|
||||
$customer_ean = $customer->getEan();
|
||||
if ($customer_ean !== null) {
|
||||
$recipient['ean'] = $customer_ean;
|
||||
}
|
||||
$public_entry_number = $customer->getPublicEntryNumber();
|
||||
if ($public_entry_number !== null) {
|
||||
$recipient['publicEntryNumber'] = $public_entry_number;
|
||||
}
|
||||
|
||||
// Send the request
|
||||
$response = $this->send_request(
|
||||
@@ -165,15 +182,7 @@ class economic_invoices_drafts_endpoint
|
||||
'currency' => $customer->getCurrency() ?? 'DKK',
|
||||
|
||||
// Set the recipient details
|
||||
'recipient' => [
|
||||
'name' => $customer_name,
|
||||
'address' => $customer_address,
|
||||
'zip' => $customer_zip,
|
||||
'city' => $customer_city,
|
||||
'vatZone' => [
|
||||
'vatZoneNumber' => (int)$customer->getVatZoneNumber(),
|
||||
],
|
||||
],
|
||||
'recipient' => $recipient,
|
||||
])
|
||||
);
|
||||
// Return the response as an object
|
||||
@@ -194,4 +203,4 @@ class economic_invoices_drafts_endpoint
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,29 @@ class economic_customer
|
||||
return $this->customer_data_object->email;
|
||||
}
|
||||
|
||||
public function getEan(): ?string
|
||||
{
|
||||
self::requireSelected();
|
||||
return $this->nullableStringField('ean');
|
||||
}
|
||||
|
||||
public function getPublicEntryNumber(): ?string
|
||||
{
|
||||
self::requireSelected();
|
||||
return $this->nullableStringField('publicEntryNumber');
|
||||
}
|
||||
|
||||
protected function nullableStringField(string $field): ?string
|
||||
{
|
||||
$value = $this->customer_data_object->{$field} ?? null;
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = trim((string)$value);
|
||||
return $normalized !== '' ? $normalized : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer address
|
||||
* @return string The customer address
|
||||
@@ -227,4 +250,4 @@ class economic_customer
|
||||
return $this->customer_data_object->vatZone->vatZoneNumber;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ use objects\orders_o;
|
||||
|
||||
class economic_invoice_draft
|
||||
{
|
||||
public const DEFAULT_LINE_BATCH_SIZE = 500;
|
||||
|
||||
/**
|
||||
* The Economic draftInvoiceNumber
|
||||
* @var int $draft_invoice_number
|
||||
@@ -112,55 +110,13 @@ class economic_invoice_draft
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the lines to the draft invoice.
|
||||
* Add the lines to the draft invoice
|
||||
* @return void
|
||||
*/
|
||||
public function addLines(): void
|
||||
{
|
||||
$this->flushLinesInBatches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add queued draft lines using chunked requests.
|
||||
*
|
||||
* @return array{line_count:int,batch_count:int,batch_sizes:array<int,int>}
|
||||
*/
|
||||
public function flushLinesInBatches(int $batch_size = self::DEFAULT_LINE_BATCH_SIZE): array
|
||||
{
|
||||
$lines = array_values($this->draft_lines);
|
||||
$line_count = count($lines);
|
||||
if ($line_count === 0) {
|
||||
return [
|
||||
'line_count' => 0,
|
||||
'batch_count' => 0,
|
||||
'batch_sizes' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$batch_size = max(1, $batch_size);
|
||||
$batch_sizes = [];
|
||||
foreach (array_chunk($lines, $batch_size) as $batch) {
|
||||
$this->sendDraftLines($batch);
|
||||
$batch_sizes[] = count($batch);
|
||||
}
|
||||
|
||||
$this->draft_lines = [];
|
||||
|
||||
return [
|
||||
'line_count' => $line_count,
|
||||
'batch_count' => count($batch_sizes),
|
||||
'batch_sizes' => $batch_sizes,
|
||||
];
|
||||
}
|
||||
|
||||
public function pendingLineCount(): int
|
||||
{
|
||||
return count($this->draft_lines);
|
||||
}
|
||||
|
||||
protected function sendDraftLines(array $draft_lines): object
|
||||
{
|
||||
$economic = new economic();
|
||||
return $economic->invoices->draft->add_lines($this->draft_invoice_number, $draft_lines);
|
||||
$economic->invoices->draft->add_lines($this->draft_invoice_number, $this->draft_lines);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,6 @@ class collected_order_invoices_o extends db
|
||||
public object_property $updated_at;
|
||||
public object_property $closed_at;
|
||||
public int $economic_wash_subscription_user_id = 1857;
|
||||
private ?array $last_economic_transfer_metrics = null;
|
||||
/**
|
||||
* The processor types
|
||||
*
|
||||
@@ -713,7 +712,6 @@ class collected_order_invoices_o extends db
|
||||
*/
|
||||
public function addInvoicesToDraft(bool $skip_check = false): self
|
||||
{
|
||||
$this->last_economic_transfer_metrics = null;
|
||||
// Require the invoice collection to be selected
|
||||
self::requireSelected();
|
||||
// Require the invoice collection to be open
|
||||
@@ -738,20 +736,10 @@ class collected_order_invoices_o extends db
|
||||
usort($orders, function ($a, $b) {
|
||||
return strtotime($a['created_at']) - strtotime($b['created_at']);
|
||||
});
|
||||
// Add the invoice lines to the draft in one accumulated batch path.
|
||||
$order_objects = [];
|
||||
// Add the invoices to the invoice draft
|
||||
foreach ( $orders as $order ) {
|
||||
$order_object = new orders_o();
|
||||
$order_object->select((int)$order['id']);
|
||||
$order_object->requireSelected();
|
||||
$order_objects[] = $order_object;
|
||||
self::addInvoiceToDraft($order['id'], true, $draft_id, $currency);
|
||||
}
|
||||
$metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency);
|
||||
$this->last_economic_transfer_metrics = [
|
||||
'draft_invoice_id' => $draft_id,
|
||||
'currency' => (string)$currency,
|
||||
...$metrics,
|
||||
];
|
||||
// If the customer has the onlyTankCleaning attribute, add the environmental fee & oil fees to the invoice draft
|
||||
self::addEnvironmentalAndOilFeesToDraft($draft_id, $currency);
|
||||
// Object changed
|
||||
@@ -760,11 +748,6 @@ class collected_order_invoices_o extends db
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastEconomicTransferMetrics(): ?array
|
||||
{
|
||||
return $this->last_economic_transfer_metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the environmental fee & oil fees to the invoice draft, if the customer has the onlyTankCleaning attribute
|
||||
* @param int $draft_id The invoice draft id
|
||||
|
||||
@@ -2595,6 +2595,12 @@ paths:
|
||||
type: string
|
||||
description: Contact person name
|
||||
example: "Mikkel"
|
||||
ean:
|
||||
type: string
|
||||
description: Optional EAN used for e-invoicing in e-conomic
|
||||
maxLength: 13
|
||||
pattern: '^[0-9]{1,13}$'
|
||||
example: "5790001234567"
|
||||
g_recaptcha_response:
|
||||
type: string
|
||||
description: reCAPTCHA verification token
|
||||
@@ -8653,6 +8659,12 @@ paths:
|
||||
email: {type: string}
|
||||
phone: {type: integer}
|
||||
name: {type: string}
|
||||
ean:
|
||||
type: string
|
||||
description: Optional EAN used for e-invoicing in e-conomic
|
||||
maxLength: 13
|
||||
pattern: '^[0-9]{1,13}$'
|
||||
example: "5790001234567"
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
|
||||
@@ -427,6 +427,7 @@ class authRoute
|
||||
$contactEmail = self::getParameter('contactEmail');
|
||||
$contactPhone = (int)self::getParameter('contactPhone');
|
||||
$contactName = self::getParameter('contactName');
|
||||
$ean = null;
|
||||
/**
|
||||
* Validate
|
||||
*/
|
||||
@@ -454,6 +455,13 @@ class authRoute
|
||||
self::requireMinValue($contactPhone, 10000000);
|
||||
self::requireMaxValue($contactPhone, 9999999999);
|
||||
}
|
||||
if (self::isParametersSet(['ean'])) {
|
||||
try {
|
||||
$ean = economic::normalizeCustomerEan(self::getParameter('ean'));
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
$response->error($exception->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the contact phone is empty, default to company phone
|
||||
@@ -545,6 +553,7 @@ class authRoute
|
||||
(int)$companyPhone,
|
||||
(int)$contactPhone,
|
||||
$companyInformation,
|
||||
$ean,
|
||||
);
|
||||
} catch (Exception $exception) {
|
||||
$recoveredCustomer = $this->recoverRegistrationAfterCreateFailure(
|
||||
|
||||
@@ -60,6 +60,14 @@ class moduleEconomicCustomerRoute
|
||||
self::requireMaxLength('phone', 255);
|
||||
self::requireMinLength('name', 1);
|
||||
self::requireMaxLength('name', 255);
|
||||
$ean = null;
|
||||
if (self::isParametersSet(['ean'])) {
|
||||
try {
|
||||
$ean = economic::normalizeCustomerEan(self::getParameter('ean'));
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
$response->error($exception->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
(new logs_o())->add('modules_economic', 'global', 1, 0, 'MODULES_ECONOMIC', 'User accessed the customer');
|
||||
$result = (new economic())->createCustomer(
|
||||
(int)self::getParameter('customer_number'),
|
||||
@@ -67,6 +75,9 @@ class moduleEconomicCustomerRoute
|
||||
(int)self::getParameter('cvr'),
|
||||
(string)self::getParameter('email'),
|
||||
(int)self::getParameter('phone'),
|
||||
null,
|
||||
null,
|
||||
$ean,
|
||||
);
|
||||
$response->success((object)$result);
|
||||
} else {
|
||||
@@ -76,4 +87,4 @@ class moduleEconomicCustomerRoute
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,46 +612,21 @@ class orderInvoicesRoute
|
||||
$preview ? 'User previewed splitting collected order invoices by month' : 'User split collected order invoices by order month'
|
||||
);
|
||||
|
||||
$date_from = $db->escape_string($date_range['dateFrom']);
|
||||
$date_to = $db->escape_string($date_range['dateTo']);
|
||||
$sql = "SELECT DISTINCT invoice_collection_id
|
||||
FROM orders
|
||||
WHERE created_at BETWEEN '$date_from' AND '$date_to'
|
||||
AND invoice_collection_id IS NOT NULL
|
||||
AND invoice_collection_id > 0
|
||||
AND deleted_at IS NULL";
|
||||
$query_result = $db->query($sql);
|
||||
$invoice_collection_ids = [];
|
||||
if (self::isParametersSet(['invoice_collection_ids'])) {
|
||||
$invoice_collection_ids_raw = self::getParameter('invoice_collection_ids');
|
||||
if (!is_array($invoice_collection_ids_raw)) {
|
||||
$response->error('invoice_collection_ids must be an array', 400);
|
||||
}
|
||||
|
||||
foreach ($invoice_collection_ids_raw as $invoice_collection_id_raw) {
|
||||
if (is_array($invoice_collection_id_raw) || is_object($invoice_collection_id_raw) || !is_numeric($invoice_collection_id_raw)) {
|
||||
$response->error('invoice_collection_ids must contain only positive integer ids', 400);
|
||||
}
|
||||
|
||||
$invoice_collection_id = (int)$invoice_collection_id_raw;
|
||||
if ($invoice_collection_id < 1 || $invoice_collection_id > 999999999) {
|
||||
$response->error('invoice_collection_ids must contain only positive integer ids', 400);
|
||||
}
|
||||
|
||||
while ($row = $query_result->fetch_assoc()) {
|
||||
$invoice_collection_id = (int)($row['invoice_collection_id'] ?? 0);
|
||||
if ($invoice_collection_id > 0) {
|
||||
$invoice_collection_ids[] = $invoice_collection_id;
|
||||
}
|
||||
|
||||
$invoice_collection_ids = array_values(array_unique($invoice_collection_ids));
|
||||
if (empty($invoice_collection_ids)) {
|
||||
$response->error('invoice_collection_ids must contain at least one id', 400);
|
||||
}
|
||||
} else {
|
||||
$date_from = $db->escape_string($date_range['dateFrom']);
|
||||
$date_to = $db->escape_string($date_range['dateTo']);
|
||||
$sql = "SELECT DISTINCT invoice_collection_id
|
||||
FROM orders
|
||||
WHERE created_at BETWEEN '$date_from' AND '$date_to'
|
||||
AND invoice_collection_id IS NOT NULL
|
||||
AND invoice_collection_id > 0
|
||||
AND deleted_at IS NULL";
|
||||
$query_result = $db->query($sql);
|
||||
while ($row = $query_result->fetch_assoc()) {
|
||||
$invoice_collection_id = (int)($row['invoice_collection_id'] ?? 0);
|
||||
if ($invoice_collection_id > 0) {
|
||||
$invoice_collection_ids[] = $invoice_collection_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$items = [];
|
||||
|
||||
@@ -85,65 +85,6 @@ it('previews monthly split changes without moving orders or creating collections
|
||||
->and(monthly_split_order_collection_id((int)$aprilOrder['id']))->toBe((int)$invoiceCollection['id']);
|
||||
});
|
||||
|
||||
it('previews only explicit monthly split invoice collection ids', function (): void {
|
||||
api_test_covers('POST /collected-invoices/split-by-month', 'preview-scope');
|
||||
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Scoped Preview Monthly Split Customer']);
|
||||
$department = api_fixtures()->createDepartment();
|
||||
$targetCollection = api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
]);
|
||||
$ignoredCollection = api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
]);
|
||||
$targetMarchOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $targetCollection['id'],
|
||||
'created_at' => '2096-03-15 10:00:00',
|
||||
]);
|
||||
$targetAprilOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $targetCollection['id'],
|
||||
'created_at' => '2096-04-02 10:00:00',
|
||||
]);
|
||||
$ignoredMarchOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $ignoredCollection['id'],
|
||||
'created_at' => '2096-03-16 10:00:00',
|
||||
]);
|
||||
$ignoredAprilOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $ignoredCollection['id'],
|
||||
'created_at' => '2096-04-03 10:00:00',
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
|
||||
|
||||
$response = api_client()->post('/collected-invoices/split-by-month', [
|
||||
'dateFrom' => '2096-03-01',
|
||||
'dateTo' => '2096-04-30',
|
||||
'invoice_collection_ids' => [$targetCollection['id']],
|
||||
'preview' => true,
|
||||
], $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$payload = $response->data();
|
||||
expect($payload['processed_count'] ?? null)->toBe(1)
|
||||
->and($payload['changed_count'] ?? null)->toBe(1)
|
||||
->and($payload['changed'][0]['invoice_collection_id'] ?? null)->toBe((int)$targetCollection['id'])
|
||||
->and(monthly_split_order_collection_id((int)$targetMarchOrder['id']))->toBe((int)$targetCollection['id'])
|
||||
->and(monthly_split_order_collection_id((int)$targetAprilOrder['id']))->toBe((int)$targetCollection['id'])
|
||||
->and(monthly_split_order_collection_id((int)$ignoredMarchOrder['id']))->toBe((int)$ignoredCollection['id'])
|
||||
->and(monthly_split_order_collection_id((int)$ignoredAprilOrder['id']))->toBe((int)$ignoredCollection['id']);
|
||||
});
|
||||
|
||||
it('splits a selected March and April collected invoice into monthly collections', function (): void {
|
||||
api_test_covers('POST /collected-invoices/split-by-month', 'happy');
|
||||
|
||||
@@ -198,74 +139,6 @@ it('splits a selected March and April collected invoice into monthly collections
|
||||
}
|
||||
});
|
||||
|
||||
it('splits only explicit monthly split invoice collection ids', function (): void {
|
||||
api_test_covers('POST /collected-invoices/split-by-month', 'scope');
|
||||
|
||||
$customer = api_fixtures()->createUser(['display_name' => 'Scoped Monthly Split Customer']);
|
||||
$department = api_fixtures()->createDepartment();
|
||||
$targetCollection = api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
'created_at' => '2096-03-01 00:00:01',
|
||||
]);
|
||||
$ignoredCollection = api_fixtures()->createInvoiceCollection([
|
||||
'customer_number' => $customer['customer_number'],
|
||||
'created_at' => '2096-03-01 00:00:01',
|
||||
]);
|
||||
$targetMarchOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $targetCollection['id'],
|
||||
'created_at' => '2096-03-15 10:00:00',
|
||||
]);
|
||||
$targetAprilOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $targetCollection['id'],
|
||||
'created_at' => '2096-04-02 10:00:00',
|
||||
]);
|
||||
$ignoredMarchOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $ignoredCollection['id'],
|
||||
'created_at' => '2096-03-16 10:00:00',
|
||||
]);
|
||||
$ignoredAprilOrder = api_fixtures()->createOrder([
|
||||
'customer_id' => $customer['customer_number'],
|
||||
'department_id' => $department['id'],
|
||||
'invoice_collection_id' => $ignoredCollection['id'],
|
||||
'created_at' => '2096-04-03 10:00:00',
|
||||
]);
|
||||
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
|
||||
$createdCollectionIds = [];
|
||||
|
||||
try {
|
||||
$response = api_client()->post('/collected-invoices/split-by-month', [
|
||||
'dateFrom' => '2096-03-01',
|
||||
'dateTo' => '2096-04-30',
|
||||
'invoice_collection_ids' => [$targetCollection['id']],
|
||||
], $session['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$payload = $response->data();
|
||||
$createdCollectionIds = (array)($payload['changed'][0]['created_invoice_collection_ids'] ?? []);
|
||||
$aprilCollectionId = (int)($createdCollectionIds[0] ?? 0);
|
||||
|
||||
expect($payload['processed_count'] ?? null)->toBe(1)
|
||||
->and($payload['changed_count'] ?? null)->toBe(1)
|
||||
->and($aprilCollectionId)->toBeGreaterThan(0)
|
||||
->and(monthly_split_order_collection_id((int)$targetMarchOrder['id']))->toBe((int)$targetCollection['id'])
|
||||
->and(monthly_split_order_collection_id((int)$targetAprilOrder['id']))->toBe($aprilCollectionId)
|
||||
->and(monthly_split_order_collection_id((int)$ignoredMarchOrder['id']))->toBe((int)$ignoredCollection['id'])
|
||||
->and(monthly_split_order_collection_id((int)$ignoredAprilOrder['id']))->toBe((int)$ignoredCollection['id']);
|
||||
} finally {
|
||||
monthly_split_cleanup_collections($createdCollectionIds);
|
||||
}
|
||||
});
|
||||
|
||||
it('sets closed_at to month end when split month has ended', function (): void {
|
||||
api_test_covers('POST /collected-invoices/split-by-month', 'closed-at');
|
||||
|
||||
@@ -472,27 +345,3 @@ it('rejects invalid monthly split date ranges', function (): void {
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false);
|
||||
});
|
||||
|
||||
it('rejects invalid explicit monthly split invoice collection ids', function (): void {
|
||||
api_test_covers('POST /collected-invoices/split-by-month', 'invalid-scope');
|
||||
|
||||
$session = api_fixtures()->createUserSession(['split_collected_invoice']);
|
||||
|
||||
api_client()->post('/collected-invoices/split-by-month', [
|
||||
'dateFrom' => '2096-03-01',
|
||||
'dateTo' => '2096-04-30',
|
||||
'invoice_collection_ids' => ['not-a-number'],
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false);
|
||||
|
||||
api_client()->post('/collected-invoices/split-by-month', [
|
||||
'dateFrom' => '2096-03-01',
|
||||
'dateTo' => '2096-04-30',
|
||||
'invoice_collection_ids' => [],
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false);
|
||||
});
|
||||
|
||||
@@ -348,60 +348,11 @@ it('creates updates lists and deactivates scoped employees without exposing raw
|
||||
->assertSuccess();
|
||||
|
||||
expect(array_column($roles->data(), 'key'))->toBe(['viewer', 'cashier', 'booking_coordinator', 'operations_lead', 'department_admin']);
|
||||
$rolesByKey = array_column($roles->data(), null, 'key');
|
||||
expect($rolesByKey['viewer']['permission_groups'] ?? null)->toBe([
|
||||
[
|
||||
'key' => 'account',
|
||||
'capabilities' => ['sign_in', 'view_own_permissions'],
|
||||
],
|
||||
]);
|
||||
$departmentAdminGroups = array_column($rolesByKey['department_admin']['permission_groups'] ?? [], 'capabilities', 'key');
|
||||
expect($departmentAdminGroups['limited_backoffice'] ?? null)->toBe([
|
||||
'open_limited_backoffice',
|
||||
'manage_department_prices',
|
||||
'manage_employee_access',
|
||||
]);
|
||||
expect($roles->body)->not->toContain('department_access_');
|
||||
$rolePayload = $roles->data();
|
||||
$rolePayloadStrings = [];
|
||||
array_walk_recursive($rolePayload, static function ($value) use (&$rolePayloadStrings): void {
|
||||
if (is_string($value)) {
|
||||
$rolePayloadStrings[] = $value;
|
||||
}
|
||||
});
|
||||
foreach ([
|
||||
'list_orders',
|
||||
'add_order',
|
||||
'edit_order',
|
||||
'delete_order',
|
||||
'list_order_items',
|
||||
'add_order_items',
|
||||
'edit_order_items',
|
||||
'delete_order_items',
|
||||
'charge_order',
|
||||
'list_bookings',
|
||||
'list_own_bookings',
|
||||
'edit_bookings',
|
||||
'add_booking',
|
||||
'complete_bookings',
|
||||
'resend_booking_confirmations',
|
||||
'department_timebookings_entries_get',
|
||||
'department_timebookings_entries_post',
|
||||
'department_timebookings_entries_put',
|
||||
'statistics_orders_new',
|
||||
'statistics_bookings_new',
|
||||
'limited_backoffice_access',
|
||||
'limited_backoffice_prices_manage',
|
||||
'limited_backoffice_employees_manage',
|
||||
] as $rawPermission) {
|
||||
expect($rolePayloadStrings)->not->toContain($rawPermission);
|
||||
}
|
||||
|
||||
$created = api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Limited Cashier',
|
||||
'email' => 'limited-cashier@example.test',
|
||||
'phone_country_code' => 45,
|
||||
'phone' => 12345678,
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'cashier',
|
||||
'department_ids' => [(int)$department['id']],
|
||||
@@ -415,9 +366,6 @@ it('creates updates lists and deactivates scoped employees without exposing raw
|
||||
$employeeId = (int)($created->data()['id'] ?? 0);
|
||||
expect($employeeId)->toBeGreaterThan(0);
|
||||
limited_backoffice_cleanup_created_employee($employeeId);
|
||||
expect($created->data()['email'] ?? null)->toBe('limited-cashier@example.test');
|
||||
expect($created->data()['phone_country_code'] ?? null)->toBe(45);
|
||||
expect($created->data()['phone'] ?? null)->toBe(12345678);
|
||||
expect($created->body)->not->toContain('department_access_');
|
||||
expect($created->body)->not->toContain('permissions');
|
||||
|
||||
@@ -436,9 +384,6 @@ it('creates updates lists and deactivates scoped employees without exposing raw
|
||||
|
||||
$updated = api_client()->put('/limited-backoffice/employees/' . $employeeId, [
|
||||
'display_name' => 'Limited Lead',
|
||||
'email' => 'limited-lead@example.test',
|
||||
'phone_country_code' => 358,
|
||||
'phone' => 87654321,
|
||||
'role_key' => 'operations_lead',
|
||||
'department_ids' => [(int)$department['id']],
|
||||
], $session['headers']);
|
||||
@@ -448,25 +393,11 @@ it('creates updates lists and deactivates scoped employees without exposing raw
|
||||
->assertSuccess();
|
||||
|
||||
expect($updated->data()['display_name'] ?? null)->toBe('Limited Lead');
|
||||
expect($updated->data()['email'] ?? null)->toBe('limited-lead@example.test');
|
||||
expect($updated->data()['phone_country_code'] ?? null)->toBe(358);
|
||||
expect($updated->data()['phone'] ?? null)->toBe(87654321);
|
||||
expect($updated->data()['role']['key'] ?? null)->toBe('operations_lead');
|
||||
|
||||
$list = api_client()->get('/limited-backoffice/employees', $session['headers']);
|
||||
$ids = array_column($list->data(), 'id');
|
||||
expect($ids)->toContain($employeeId);
|
||||
$listedEmployee = null;
|
||||
foreach ($list->data() as $employee) {
|
||||
if ((int)($employee['id'] ?? 0) === $employeeId) {
|
||||
$listedEmployee = $employee;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect($listedEmployee)->not->toBeNull();
|
||||
expect($listedEmployee['email'] ?? null)->toBe('limited-lead@example.test');
|
||||
expect($listedEmployee['phone_country_code'] ?? null)->toBe(358);
|
||||
expect($listedEmployee['phone'] ?? null)->toBe(87654321);
|
||||
expect($list->body)->not->toContain('department_access_');
|
||||
|
||||
$deactivated = api_client()->delete('/limited-backoffice/employees/' . $employeeId, null, $session['headers']);
|
||||
@@ -488,34 +419,6 @@ it('creates updates lists and deactivates scoped employees without exposing raw
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts employees without optional phone details', function (): void {
|
||||
api_test_covers('POST /limited-backoffice/employees', 'happy');
|
||||
|
||||
$department = api_fixtures()->createDepartment(['name' => 'Limited Employee No Phone']);
|
||||
$session = limited_backoffice_manager_session([(int)$department['id']]);
|
||||
|
||||
$created = api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Limited No Phone',
|
||||
'email' => 'limited-no-phone@example.test',
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'viewer',
|
||||
'department_ids' => [(int)$department['id']],
|
||||
], $session['headers']);
|
||||
|
||||
$created
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$employeeId = (int)($created->data()['id'] ?? 0);
|
||||
expect($employeeId)->toBeGreaterThan(0);
|
||||
limited_backoffice_cleanup_created_employee($employeeId);
|
||||
expect(array_key_exists('phone_country_code', $created->data()))->toBeTrue();
|
||||
expect(array_key_exists('phone', $created->data()))->toBeTrue();
|
||||
expect($created->data()['phone_country_code'])->toBeNull();
|
||||
expect($created->data()['phone'])->toBeNull();
|
||||
});
|
||||
|
||||
it('rejects employee scopes roles raw permissions self edits superusers and shared groups', function (): void {
|
||||
api_test_covers('POST /limited-backoffice/employees', 'validation');
|
||||
api_test_covers('PUT /limited-backoffice/employees/{employeeId}', 'validation');
|
||||
@@ -526,7 +429,6 @@ it('rejects employee scopes roles raw permissions self edits superusers and shar
|
||||
|
||||
api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Outside Employee',
|
||||
'email' => 'outside@example.test',
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'cashier',
|
||||
'department_ids' => [(int)$otherDepartment['id']],
|
||||
@@ -538,7 +440,6 @@ it('rejects employee scopes roles raw permissions self edits superusers and shar
|
||||
|
||||
api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Raw Employee',
|
||||
'email' => 'raw@example.test',
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'cashier',
|
||||
'department_ids' => [(int)$department['id']],
|
||||
@@ -551,7 +452,6 @@ it('rejects employee scopes roles raw permissions self edits superusers and shar
|
||||
|
||||
api_client()->post('/limited-backoffice/employees', [
|
||||
'display_name' => 'Unknown Role Employee',
|
||||
'email' => 'unknown-role@example.test',
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'superuser',
|
||||
'department_ids' => [(int)$department['id']],
|
||||
@@ -604,88 +504,3 @@ it('rejects employee scopes roles raw permissions self edits superusers and shar
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Cannot manage shared groups.');
|
||||
});
|
||||
|
||||
it('rejects invalid limited backoffice employee contact details', function (): void {
|
||||
api_test_covers('POST /limited-backoffice/employees', 'validation');
|
||||
api_test_covers('PUT /limited-backoffice/employees/{employeeId}', 'validation');
|
||||
|
||||
$department = api_fixtures()->createDepartment(['name' => 'Limited Employee Contact Validation']);
|
||||
$session = limited_backoffice_manager_session([(int)$department['id']]);
|
||||
$basePayload = [
|
||||
'display_name' => 'Contact Employee',
|
||||
'email' => 'contact@example.test',
|
||||
'password' => 'Secret123!',
|
||||
'role_key' => 'viewer',
|
||||
'department_ids' => [(int)$department['id']],
|
||||
];
|
||||
|
||||
api_client()->post('/limited-backoffice/employees', array_diff_key($basePayload, ['email' => true]), $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Email is required.');
|
||||
|
||||
api_client()->post('/limited-backoffice/employees', [
|
||||
...$basePayload,
|
||||
'email' => 'not-an-email',
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Email must be a valid email address.');
|
||||
|
||||
api_client()->post('/limited-backoffice/employees', [
|
||||
...$basePayload,
|
||||
'phone_country_code' => 45,
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Phone country code and phone number must be provided together.');
|
||||
|
||||
api_client()->post('/limited-backoffice/employees', [
|
||||
...$basePayload,
|
||||
'phone_country_code' => 1,
|
||||
'phone' => 12345678,
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Phone country code is not supported.');
|
||||
|
||||
api_client()->post('/limited-backoffice/employees', [
|
||||
...$basePayload,
|
||||
'phone_country_code' => 45,
|
||||
'phone' => '12ab',
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Phone values must contain digits only.');
|
||||
|
||||
$created = api_client()->post('/limited-backoffice/employees', $basePayload, $session['headers'])
|
||||
->assertStatus(200)
|
||||
->assertEnvelope()
|
||||
->assertSuccess();
|
||||
|
||||
$employeeId = (int)($created->data()['id'] ?? 0);
|
||||
expect($employeeId)->toBeGreaterThan(0);
|
||||
limited_backoffice_cleanup_created_employee($employeeId);
|
||||
|
||||
api_client()->put('/limited-backoffice/employees/' . $employeeId, [
|
||||
'email' => '',
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Email is required.');
|
||||
|
||||
api_client()->put('/limited-backoffice/employees/' . $employeeId, [
|
||||
'phone_country_code' => 45,
|
||||
'phone' => '123',
|
||||
], $session['headers'])
|
||||
->assertStatus(400)
|
||||
->assertEnvelope()
|
||||
->assertSuccess(false)
|
||||
->assertMessage('Phone number must be 4-15 digits.');
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ it('returns the raw upstream create response and preserves the requested payload
|
||||
expect($probe->inner->lastPayload['phone'])->toBe(42331123);
|
||||
expect($probe->inner->lastPayload['telephoneAndFaxNumber'])->toBe('42331123');
|
||||
expect($probe->inner->lastPayload['mobilePhone'])->toBe('42331123');
|
||||
expect(array_key_exists('ean', $probe->inner->lastPayload))->toBeFalse();
|
||||
});
|
||||
|
||||
it('adds supported CVR company fields to the e-conomic customer payload', function (): void {
|
||||
@@ -97,3 +98,46 @@ it('adds supported CVR company fields to the e-conomic customer payload', functi
|
||||
expect($probe->inner->lastPayload['mobilePhone'])->toBe('55667788');
|
||||
expect(array_key_exists('industrycode', $probe->inner->lastPayload))->toBeFalse();
|
||||
});
|
||||
|
||||
it('adds a normalized EAN to the e-conomic customer payload when provided', function (): void {
|
||||
$stubResponse = (object)[
|
||||
'customerNumber' => 42331123,
|
||||
'name' => 'Truckwash ApS',
|
||||
];
|
||||
|
||||
$probe = new EconomicCreateCustomerProbe($stubResponse);
|
||||
$probe->createCustomer(
|
||||
42331123,
|
||||
'Truckwash ApS',
|
||||
37781258,
|
||||
'invoice@truckwash.test',
|
||||
42331123,
|
||||
null,
|
||||
null,
|
||||
'57 90-001234567',
|
||||
);
|
||||
|
||||
expect($probe->inner->lastPayload['ean'])->toBe('5790001234567');
|
||||
});
|
||||
|
||||
it('rejects EAN values longer than e-conomic accepts', function (): void {
|
||||
$stubResponse = (object)[
|
||||
'customerNumber' => 42331123,
|
||||
'name' => 'Truckwash ApS',
|
||||
];
|
||||
|
||||
$probe = new EconomicCreateCustomerProbe($stubResponse);
|
||||
$call = static fn() => $probe->createCustomer(
|
||||
42331123,
|
||||
'Truckwash ApS',
|
||||
37781258,
|
||||
'invoice@truckwash.test',
|
||||
42331123,
|
||||
null,
|
||||
null,
|
||||
'57900012345678',
|
||||
);
|
||||
|
||||
expect($call)->toThrow(InvalidArgumentException::class, 'EAN must be at most 13 digits.');
|
||||
expect($probe->inner->lastPayload)->toBe([]);
|
||||
});
|
||||
|
||||
@@ -176,6 +176,22 @@ it('creates a new e-conomic customer and returns a created result for new rows',
|
||||
expect($result['has_account'])->toBeFalse();
|
||||
});
|
||||
|
||||
it('rejects EAN values longer than e-conomic accepts before creating customers', function (): void {
|
||||
$service = new CustomerMassImportServiceProbe();
|
||||
|
||||
$call = static fn() => $service->import([
|
||||
'cvr' => '29424764',
|
||||
'name' => 'TGP TRANSPORT APS',
|
||||
'email' => 'tgp@example.com',
|
||||
'ean' => '57900012345678',
|
||||
'phone' => '22725567',
|
||||
]);
|
||||
|
||||
expect($call)->toThrow(RuntimeException::class, 'EAN must be at most 13 digits.');
|
||||
expect($service->createCalls)->toBe([]);
|
||||
expect($service->bootstrapCalls)->toBe([]);
|
||||
});
|
||||
|
||||
it('creates the economic record for an existing local account when no matching upstream customer exists', function (): void {
|
||||
$service = new CustomerMassImportServiceProbe();
|
||||
$service->localExists = true;
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
it('routes collected invoice draft line uploads through the multi-order batch endpoint', function (): void {
|
||||
$content = file_get_contents(dirname(__DIR__, 3) . '/objects/collected_order_invoices_o.php');
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
$content = (string)$content;
|
||||
|
||||
$start = strpos($content, 'public function addInvoicesToDraft');
|
||||
$end = strpos($content, 'public function getLastEconomicTransferMetrics');
|
||||
expect($start)->not->toBeFalse();
|
||||
expect($end)->not->toBeFalse();
|
||||
expect($end)->toBeGreaterThan($start);
|
||||
|
||||
$methodBlock = substr($content, (int)$start, (int)$end - (int)$start);
|
||||
expect($methodBlock)->toContain('$order_objects = [];')
|
||||
->and($methodBlock)->toContain('$metrics = (new economic())->invoices->draft->add_orders($draft_id, $order_objects, $currency);')
|
||||
->and($methodBlock)->toContain('...$metrics')
|
||||
->and($methodBlock)->not->toContain('self::addInvoiceToDraft($order[\'id\'], true, $draft_id, $currency);');
|
||||
});
|
||||
|
||||
it('keeps single-order draft uploads as a wrapper around the batch endpoint', function (): void {
|
||||
$content = file_get_contents(dirname(__DIR__, 3) . '/modules/economic/endpoints/invoices/draft/economic_invoices_draft_endpoint.php');
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
$content = (string)$content;
|
||||
|
||||
$singleStart = strpos($content, 'public function add_order');
|
||||
$singleEnd = strpos($content, 'public function add_orders');
|
||||
expect($singleStart)->not->toBeFalse();
|
||||
expect($singleEnd)->not->toBeFalse();
|
||||
expect($singleEnd)->toBeGreaterThan($singleStart);
|
||||
|
||||
$singleBlock = substr($content, (int)$singleStart, (int)$singleEnd - (int)$singleStart);
|
||||
expect($singleBlock)->toContain('$this->add_orders($invoiceDraftId, [$order], $currency);');
|
||||
|
||||
$batchBlock = substr($content, (int)$singleEnd);
|
||||
expect($batchBlock)->toContain('$draftInvoice->flushLinesInBatches($line_batch_size);')
|
||||
->and($batchBlock)->toContain("'orders_with_invoice_lines' => \$orders_with_invoice_lines");
|
||||
});
|
||||
|
||||
it('includes collected invoice batch transfer metrics in queue results when available', function (): void {
|
||||
$content = file_get_contents(dirname(__DIR__, 3) . '/classes/economic_transfer_executor.php');
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
$content = (string)$content;
|
||||
|
||||
expect($content)->toContain('$transfer_metrics = $collected_order_invoices->getLastEconomicTransferMetrics();')
|
||||
->and($content)->toContain("\$result['economic_transfer_metrics'] = \$transfer_metrics;");
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
app_require('modules/economic/helpers/economic_customer.php');
|
||||
|
||||
use helpers\economic_customer;
|
||||
|
||||
function economic_customer_helper_from_payload(object $payload): economic_customer
|
||||
{
|
||||
$reflection = new ReflectionClass(economic_customer::class);
|
||||
/** @var economic_customer $customer */
|
||||
$customer = $reflection->newInstanceWithoutConstructor();
|
||||
|
||||
$property = $reflection->getProperty('customer_data_object');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue($customer, $payload);
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
it('exposes optional EAN and public entry number from fetched e-conomic customer data', function (): void {
|
||||
$customer = economic_customer_helper_from_payload((object)[
|
||||
'customerNumber' => 42331123,
|
||||
'ean' => ' 5790001234567 ',
|
||||
'publicEntryNumber' => ' DK123456789 ',
|
||||
]);
|
||||
|
||||
expect($customer->getEan())->toBe('5790001234567');
|
||||
expect($customer->getPublicEntryNumber())->toBe('DK123456789');
|
||||
});
|
||||
|
||||
it('returns null for blank optional e-conomic customer recipient identifiers', function (): void {
|
||||
$customer = economic_customer_helper_from_payload((object)[
|
||||
'customerNumber' => 42331123,
|
||||
'ean' => ' ',
|
||||
'publicEntryNumber' => '',
|
||||
]);
|
||||
|
||||
expect($customer->getEan())->toBeNull();
|
||||
expect($customer->getPublicEntryNumber())->toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
function economic_ean_openapi_content_or_skip(): string
|
||||
{
|
||||
$candidates = [];
|
||||
for ($depth = 1; $depth <= 8; $depth++) {
|
||||
$candidates[] = dirname(__DIR__, $depth) . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
}
|
||||
|
||||
$cwd = getcwd();
|
||||
if (is_string($cwd) && $cwd !== '') {
|
||||
$candidates[] = $cwd . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
$candidates[] = dirname($cwd) . DIRECTORY_SEPARATOR . 'openapi.yaml';
|
||||
}
|
||||
|
||||
foreach (array_values(array_unique($candidates)) as $candidate) {
|
||||
if (is_file($candidate)) {
|
||||
$content = file_get_contents($candidate);
|
||||
if ($content !== false) {
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test()->markTestSkipped('openapi.yaml is not available in this runtime environment.');
|
||||
}
|
||||
|
||||
function economic_ean_openapi_block(string $content, string $start, string $end): string
|
||||
{
|
||||
$start_pos = strpos($content, $start);
|
||||
$end_pos = strpos($content, $end);
|
||||
expect($start_pos)->not->toBeFalse();
|
||||
expect($end_pos)->not->toBeFalse();
|
||||
expect($end_pos)->toBeGreaterThan($start_pos);
|
||||
|
||||
return substr($content, (int)$start_pos, (int)$end_pos - (int)$start_pos);
|
||||
}
|
||||
|
||||
it('documents optional EAN on customer creation endpoints', function (): void {
|
||||
$content = economic_ean_openapi_content_or_skip();
|
||||
|
||||
$register_block = economic_ean_openapi_block($content, '/auth/register/cvr:', '/auth/password-reset/request:');
|
||||
$economic_customer_block = economic_ean_openapi_block($content, '/modules/economic/customer:', '/economic/layouts:');
|
||||
|
||||
foreach ([$register_block, $economic_customer_block] as $block) {
|
||||
expect($block)->toContain('ean:');
|
||||
expect($block)->toContain('maxLength: 13');
|
||||
expect($block)->toContain("pattern: '^[0-9]{1,13}$'");
|
||||
}
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
use helpers\economic_invoice_draft;
|
||||
|
||||
if (!class_exists('EconomicInvoiceDraftBatchingProbe')) {
|
||||
class EconomicInvoiceDraftBatchingProbe extends economic_invoice_draft
|
||||
{
|
||||
public array $sentBatches = [];
|
||||
|
||||
protected function sendDraftLines(array $draft_lines): object
|
||||
{
|
||||
$this->sentBatches[] = $draft_lines;
|
||||
return (object)['lines' => $draft_lines];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('EconomicInvoiceDraftFailingBatchingProbe')) {
|
||||
class EconomicInvoiceDraftFailingBatchingProbe extends EconomicInvoiceDraftBatchingProbe
|
||||
{
|
||||
public int $failOnBatch = 1;
|
||||
|
||||
protected function sendDraftLines(array $draft_lines): object
|
||||
{
|
||||
if (count($this->sentBatches) + 1 === $this->failOnBatch) {
|
||||
throw new RuntimeException('Simulated e-conomic line batch failure');
|
||||
}
|
||||
|
||||
return parent::sendDraftLines($draft_lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('does not call e-conomic when flushing an empty draft line buffer', function (): void {
|
||||
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||
|
||||
$metrics = $draft->flushLinesInBatches();
|
||||
|
||||
expect($metrics)->toBe([
|
||||
'line_count' => 0,
|
||||
'batch_count' => 0,
|
||||
'batch_sizes' => [],
|
||||
])->and($draft->sentBatches)->toBe([]);
|
||||
});
|
||||
|
||||
it('flushes a small draft line buffer in one request and clears pending lines', function (): void {
|
||||
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||
$draft->addTextLine('line-0');
|
||||
$draft->addTextLine('line-1');
|
||||
$draft->addTextLine('line-2');
|
||||
|
||||
$metrics = $draft->flushLinesInBatches(500);
|
||||
|
||||
expect($metrics)->toBe([
|
||||
'line_count' => 3,
|
||||
'batch_count' => 1,
|
||||
'batch_sizes' => [3],
|
||||
])->and($draft->sentBatches)->toHaveCount(1)
|
||||
->and($draft->sentBatches[0][0]['description'])->toBe('line-0')
|
||||
->and($draft->sentBatches[0][2]['description'])->toBe('line-2')
|
||||
->and($draft->pendingLineCount())->toBe(0);
|
||||
});
|
||||
|
||||
it('chunks large draft line buffers while preserving line order', function (): void {
|
||||
$draft = new EconomicInvoiceDraftBatchingProbe(123, 'DKK', true);
|
||||
for ($i = 0; $i < 1201; $i++) {
|
||||
$draft->addTextLine('line-' . $i);
|
||||
}
|
||||
|
||||
$metrics = $draft->flushLinesInBatches(500);
|
||||
|
||||
expect($metrics)->toBe([
|
||||
'line_count' => 1201,
|
||||
'batch_count' => 3,
|
||||
'batch_sizes' => [500, 500, 201],
|
||||
])->and($draft->sentBatches)->toHaveCount(3)
|
||||
->and($draft->sentBatches[0][0]['description'])->toBe('line-0')
|
||||
->and($draft->sentBatches[1][0]['description'])->toBe('line-500')
|
||||
->and($draft->sentBatches[2][200]['description'])->toBe('line-1200')
|
||||
->and($draft->pendingLineCount())->toBe(0);
|
||||
});
|
||||
|
||||
it('bubbles line batch failures and keeps pending lines available', function (): void {
|
||||
$draft = new EconomicInvoiceDraftFailingBatchingProbe(123, 'DKK', true);
|
||||
$draft->addTextLine('line-0');
|
||||
|
||||
expect(fn () => $draft->flushLinesInBatches(500))
|
||||
->toThrow(RuntimeException::class, 'Simulated e-conomic line batch failure');
|
||||
|
||||
expect($draft->sentBatches)->toBe([])
|
||||
->and($draft->pendingLineCount())->toBe(1);
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
it('wires EAN and public entry number into e-conomic invoice draft recipients', function (): void {
|
||||
$content = file_get_contents(app_path('modules/economic/endpoints/invoices/economic_invoices_drafts_endpoint.php'));
|
||||
|
||||
expect($content)->not->toBeFalse();
|
||||
expect($content)->toContain('$customer->getEan()');
|
||||
expect($content)->toContain("\$recipient['ean']");
|
||||
expect($content)->toContain('$customer->getPublicEntryNumber()');
|
||||
expect($content)->toContain("\$recipient['publicEntryNumber']");
|
||||
expect($content)->toContain("'recipient' => \$recipient");
|
||||
});
|
||||
@@ -18,6 +18,8 @@ it('parses cached economic customer payloads that use snake_case customer_number
|
||||
'customer_number' => '42331123',
|
||||
'name' => 'Truckwash ApS',
|
||||
'email' => 'jb@truckwash.dk',
|
||||
'ean' => '5790001234567',
|
||||
'public_entry_number' => 'DK123456789',
|
||||
'currency' => 'DKK',
|
||||
'country' => 'DK',
|
||||
'barred' => true,
|
||||
@@ -31,6 +33,8 @@ it('parses cached economic customer payloads that use snake_case customer_number
|
||||
'zip' => null,
|
||||
'corporateIdentificationNumber' => null,
|
||||
'email' => 'jb@truckwash.dk',
|
||||
'ean' => '5790001234567',
|
||||
'publicEntryNumber' => 'DK123456789',
|
||||
'mobilePhone' => null,
|
||||
'currency' => 'DKK',
|
||||
'country' => 'DK',
|
||||
|
||||
@@ -105,7 +105,30 @@ namespace classes {
|
||||
};
|
||||
}
|
||||
|
||||
public function createCustomer($number, $name, $cvr, $email, $phone, $mobilePhone = null, $companyInformation = null): object
|
||||
public static function normalizeCustomerEan(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$digits = preg_replace('/\D+/', '', (string)$value);
|
||||
if (!is_string($digits)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$digits = trim($digits);
|
||||
if ($digits === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strlen($digits) > 13) {
|
||||
throw new \InvalidArgumentException('EAN must be at most 13 digits.');
|
||||
}
|
||||
|
||||
return $digits;
|
||||
}
|
||||
|
||||
public function createCustomer($number, $name, $cvr, $email, $phone, $mobilePhone = null, $companyInformation = null, $ean = null): object
|
||||
{
|
||||
self::$create_calls[] = [
|
||||
'number' => (int)$number,
|
||||
@@ -115,6 +138,7 @@ namespace classes {
|
||||
'phone' => (int)$phone,
|
||||
'mobile_phone' => $mobilePhone === null ? null : (int)$mobilePhone,
|
||||
'company_information' => $companyInformation,
|
||||
'ean' => $ean === null ? null : (string)$ean,
|
||||
];
|
||||
|
||||
if (self::$mock_create_exception !== null) {
|
||||
@@ -424,6 +448,16 @@ namespace {
|
||||
'expected_error' => 'Parameter cvr must be at least 8 characters long',
|
||||
'expected_status' => 400,
|
||||
],
|
||||
[
|
||||
'name' => 'Invalid EAN length (too long)',
|
||||
'params' => array_merge($baseParams, ['ean' => '57900012345678']),
|
||||
'expected_error' => 'EAN must be at most 13 digits.',
|
||||
'expected_status' => 400,
|
||||
'assert' => static function (): void {
|
||||
assert_true(count(\classes\economic::$create_calls) === 0, 'Invalid EAN must not create e-conomic customers.');
|
||||
assert_true(count(\classes\email::$sent) === 0, 'Invalid EAN must not send welcome emails.');
|
||||
},
|
||||
],
|
||||
[
|
||||
'name' => 'CVR lookup failure returns validation error without creating customer',
|
||||
'params' => array_merge($baseParams, ['cvr' => '11111112']),
|
||||
@@ -563,7 +597,7 @@ namespace {
|
||||
],
|
||||
[
|
||||
'name' => 'Successful registration bootstraps local user before welcome emails',
|
||||
'params' => array_merge($baseParams, ['contactPhone' => 87654320]),
|
||||
'params' => array_merge($baseParams, ['contactPhone' => 87654320, 'ean' => '57 90-001234567']),
|
||||
'setup' => static function (): void {
|
||||
\classes\economic::$mock_create_response = (object)[
|
||||
'customerNumber' => 12345678,
|
||||
@@ -579,6 +613,7 @@ namespace {
|
||||
assert_true(count(\classes\economic::$create_calls) === 1, 'Fresh registration must call create exactly once.');
|
||||
assert_true(\classes\economic::$create_calls[0]['phone'] === 12345678, 'Fresh registration must use the company phone as the e-conomic customer phone.');
|
||||
assert_true(\classes\economic::$create_calls[0]['mobile_phone'] === 87654320, 'Fresh registration must pass the contact phone as the e-conomic mobile phone.');
|
||||
assert_true(\classes\economic::$create_calls[0]['ean'] === '5790001234567', 'Fresh registration must pass normalized EAN to e-conomic.');
|
||||
$companyInformation = \classes\economic::$create_calls[0]['company_information'];
|
||||
assert_true(is_object($companyInformation), 'Fresh registration must pass CVR company information to e-conomic.');
|
||||
assert_true($companyInformation->address === 'Demo Street 1', 'Fresh registration must pass the CVR address to e-conomic.');
|
||||
|
||||
Reference in New Issue
Block a user