340 lines
11 KiB
PHP
340 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
function lookup_method_id(PDO $pdo, string $table, ?string $code): ?int
|
|
{
|
|
if ($code === null || $code === '') {
|
|
return null;
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT id FROM public.{$table} WHERE code = :code LIMIT 1");
|
|
$stmt->execute([':code' => $code]);
|
|
$id = $stmt->fetchColumn();
|
|
|
|
return $id === false ? null : (int) $id;
|
|
}
|
|
|
|
function map_payment_code(string $input): ?string
|
|
{
|
|
$v = strtolower(trim($input));
|
|
if ($v === '') {
|
|
return null;
|
|
}
|
|
|
|
if (str_contains($v, 'twint')) {
|
|
return 'twint';
|
|
}
|
|
if (str_contains($v, 'bank') || str_contains($v, 'vorauskasse') || str_contains($v, 'ueberweisung')) {
|
|
return 'bank_transfer';
|
|
}
|
|
if (str_contains($v, 'kredit') || str_contains($v, 'debit') || str_contains($v, 'card')) {
|
|
return 'card';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function map_shipping_code(string $input): ?string
|
|
{
|
|
$v = strtolower(trim($input));
|
|
if ($v === '') {
|
|
return null;
|
|
}
|
|
|
|
if (str_contains($v, 'abholung') || str_contains($v, 'pickup')) {
|
|
return 'pickup';
|
|
}
|
|
if (str_contains($v, 'post') || str_contains($v, 'versand')) {
|
|
return 'post_standard';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function ensure_required_tables_exist(PDO $pdo): void
|
|
{
|
|
$required = [
|
|
'party',
|
|
'address',
|
|
'sales_order',
|
|
'sales_order_line',
|
|
'payment_method',
|
|
'shipping_method',
|
|
];
|
|
|
|
$stmt = $pdo->query(
|
|
"SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'"
|
|
);
|
|
$rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
|
$existing = array_map('strval', $rows ?: []);
|
|
$missing = array_values(array_diff($required, $existing));
|
|
|
|
if ($missing !== []) {
|
|
throw new RuntimeException('DB schema not initialized. Missing tables: ' . implode(', ', $missing));
|
|
}
|
|
}
|
|
|
|
function find_existing_order_id(PDO $pdo, string $externalRef): ?int
|
|
{
|
|
$stmt = $pdo->prepare('SELECT id FROM sales_order WHERE external_ref = :external_ref LIMIT 1');
|
|
$stmt->execute([':external_ref' => $externalRef]);
|
|
$id = $stmt->fetchColumn();
|
|
|
|
return $id === false ? null : (int) $id;
|
|
}
|
|
|
|
function upsert_sales_order(PDO $pdo, array $fields): int
|
|
{
|
|
$stmt = $pdo->prepare(
|
|
'INSERT INTO public.sales_order (
|
|
external_ref, party_id, order_source, order_status, payment_status, payment_method_id, shipping_method_id,
|
|
amount_net, amount_shipping, amount_tax, amount_discount, total_amount, currency, webhook_payload, imported_at, created_at, updated_at
|
|
) VALUES (
|
|
:external_ref, :party_id, :order_source, :order_status, :payment_status, :payment_method_id, :shipping_method_id,
|
|
:amount_net, :amount_shipping, :amount_tax, :amount_discount, :total_amount, :currency, :webhook_payload::jsonb, NOW(), NOW(), NOW()
|
|
)
|
|
ON CONFLICT (external_ref) DO UPDATE SET
|
|
party_id = EXCLUDED.party_id,
|
|
order_source = EXCLUDED.order_source,
|
|
order_status = EXCLUDED.order_status,
|
|
payment_status = EXCLUDED.payment_status,
|
|
payment_method_id = EXCLUDED.payment_method_id,
|
|
shipping_method_id = EXCLUDED.shipping_method_id,
|
|
amount_net = EXCLUDED.amount_net,
|
|
amount_shipping = EXCLUDED.amount_shipping,
|
|
amount_tax = EXCLUDED.amount_tax,
|
|
amount_discount = EXCLUDED.amount_discount,
|
|
total_amount = EXCLUDED.total_amount,
|
|
currency = EXCLUDED.currency,
|
|
webhook_payload = EXCLUDED.webhook_payload,
|
|
imported_at = NOW(),
|
|
updated_at = NOW()
|
|
RETURNING id'
|
|
);
|
|
$stmt->execute($fields);
|
|
|
|
$id = $stmt->fetchColumn();
|
|
if ($id === false) {
|
|
throw new RuntimeException('Could not upsert order');
|
|
}
|
|
|
|
return (int) $id;
|
|
}
|
|
|
|
function create_direct_sales_order(PDO $pdo, array $fields): array
|
|
{
|
|
$orderDate = trim((string) ($fields[':order_date'] ?? ''));
|
|
if ($orderDate === '') {
|
|
throw new RuntimeException('Missing order date');
|
|
}
|
|
|
|
$stmt = $pdo->prepare(
|
|
"WITH next_id AS (
|
|
SELECT nextval(pg_get_serial_sequence('sales_order', 'id')) AS id
|
|
)
|
|
INSERT INTO sales_order (
|
|
id, external_ref, party_id, order_source, order_date, order_status, payment_status, payment_method_id,
|
|
amount_net, amount_shipping, amount_tax, amount_discount, total_amount, currency, imported_at, created_at, updated_at
|
|
)
|
|
SELECT
|
|
next_id.id,
|
|
'DIR-' || TO_CHAR(CAST(:order_date AS timestamp), 'YYYYMMDD') || '-' || LPAD(next_id.id::text, 5, '0'),
|
|
:party_id, 'direct', :order_date, 'imported', 'paid', :payment_method_id,
|
|
:amount_net, 0, 0, 0, :total_amount, 'CHF', NOW(), NOW(), NOW()
|
|
FROM next_id
|
|
RETURNING id, external_ref"
|
|
);
|
|
$stmt->execute($fields);
|
|
|
|
$row = $stmt->fetch();
|
|
if ($row === false) {
|
|
throw new RuntimeException('Could not create order');
|
|
}
|
|
|
|
return [
|
|
'id' => (int) $row['id'],
|
|
'external_ref' => (string) $row['external_ref'],
|
|
];
|
|
}
|
|
|
|
function delete_sales_order_lines(PDO $pdo, int $orderId): void
|
|
{
|
|
$deleteLines = $pdo->prepare('DELETE FROM public.sales_order_line WHERE sales_order_id = :sales_order_id');
|
|
$deleteLines->execute([':sales_order_id' => $orderId]);
|
|
}
|
|
|
|
function insert_sales_order_line(PDO $pdo, array $fields): int
|
|
{
|
|
$stmt = $pdo->prepare(
|
|
'INSERT INTO public.sales_order_line (
|
|
sales_order_id, line_no, sellable_item_id, raw_external_article_number, raw_external_title,
|
|
qty, unit_price, line_total, created_at, updated_at
|
|
) VALUES (
|
|
:sales_order_id, :line_no, :sellable_item_id, :article_number, :title,
|
|
:qty, :unit_price, :line_total, NOW(), NOW()
|
|
)
|
|
RETURNING id'
|
|
);
|
|
$stmt->execute($fields);
|
|
|
|
$id = $stmt->fetchColumn();
|
|
if ($id === false) {
|
|
throw new RuntimeException('Could not insert sales_order_line');
|
|
}
|
|
|
|
return (int) $id;
|
|
}
|
|
|
|
function normalize_sales_order_sort_column(string $sortColumn): string
|
|
{
|
|
$allowed = [
|
|
'order_date',
|
|
'external_ref',
|
|
'name',
|
|
'total_amount',
|
|
];
|
|
|
|
return in_array($sortColumn, $allowed, true) ? $sortColumn : 'order_date';
|
|
}
|
|
|
|
function normalize_sales_order_sort_direction(string $direction, string $sortColumn): string
|
|
{
|
|
$direction = strtoupper(trim($direction));
|
|
if ($direction !== 'ASC' && $direction !== 'DESC') {
|
|
return $sortColumn === 'order_date' ? 'DESC' : 'ASC';
|
|
}
|
|
|
|
return $direction;
|
|
}
|
|
|
|
function escape_sales_order_search_term(string $searchTerm): string
|
|
{
|
|
return strtr($searchTerm, [
|
|
'\\' => '\\\\',
|
|
'%' => '\\%',
|
|
'_' => '\\_',
|
|
]);
|
|
}
|
|
|
|
function get_sales_order_overview(PDO $pdo, array $filters = []): array
|
|
{
|
|
$searchTerm = trim((string) ($filters['search'] ?? ''));
|
|
$sortColumn = normalize_sales_order_sort_column((string) ($filters['sort_column'] ?? 'order_date'));
|
|
$sortDirection = normalize_sales_order_sort_direction((string) ($filters['sort_direction'] ?? ''), $sortColumn);
|
|
$limit = max(1, (int) ($filters['limit'] ?? 20));
|
|
$pageSize = max(1, (int) ($filters['page_size'] ?? 20));
|
|
|
|
$filterSql = '';
|
|
$params = [];
|
|
if ($searchTerm !== '') {
|
|
$filterSql = <<<'SQL'
|
|
WHERE (
|
|
so.external_ref ILIKE :search_term ESCAPE '\'
|
|
OR so.order_date::text ILIKE :search_term ESCAPE '\'
|
|
OR COALESCE(so.total_amount::text, '') ILIKE :search_term ESCAPE '\'
|
|
OR COALESCE(ad.last_name, '') ILIKE :search_term ESCAPE '\'
|
|
OR COALESCE(ad.first_name, '') ILIKE :search_term ESCAPE '\'
|
|
OR (COALESCE(ad.first_name, '') || ' ' || COALESCE(ad.last_name, '')) ILIKE :search_term ESCAPE '\'
|
|
OR COALESCE(ad.street, '') ILIKE :search_term ESCAPE '\'
|
|
OR COALESCE(ad.house_number, '') ILIKE :search_term ESCAPE '\'
|
|
OR COALESCE(ad.zip, '') ILIKE :search_term ESCAPE '\'
|
|
OR COALESCE(ad.city, '') ILIKE :search_term ESCAPE '\'
|
|
OR COALESCE(ad.country_name, '') ILIKE :search_term ESCAPE '\'
|
|
)
|
|
SQL;
|
|
$params[':search_term'] = '%' . escape_sales_order_search_term($searchTerm) . '%';
|
|
}
|
|
|
|
$sortExpressions = [
|
|
'order_date' => 'so.order_date',
|
|
'external_ref' => 'so.external_ref',
|
|
'name' => 'COALESCE(ad.first_name, \'\') || \' \' || COALESCE(ad.last_name, \'\')',
|
|
'total_amount' => 'COALESCE(so.total_amount, 0)',
|
|
];
|
|
$sortExpression = $sortExpressions[$sortColumn] ?? 'so.order_date';
|
|
|
|
$baseFromSql = <<<'SQL'
|
|
FROM sales_order so
|
|
LEFT JOIN LATERAL (
|
|
SELECT
|
|
a.first_name,
|
|
a.last_name,
|
|
a.street,
|
|
a.house_number,
|
|
a.zip,
|
|
a.city,
|
|
a.country_name
|
|
FROM address a
|
|
WHERE a.party_id = so.party_id
|
|
AND a.type = 'shipping'
|
|
ORDER BY a.id DESC
|
|
LIMIT 1
|
|
) ad ON TRUE
|
|
SQL;
|
|
|
|
$countStmt = $pdo->prepare(
|
|
'SELECT COUNT(*) ' . $baseFromSql . "\n" . $filterSql
|
|
);
|
|
foreach ($params as $key => $value) {
|
|
$countStmt->bindValue($key, $value, PDO::PARAM_STR);
|
|
}
|
|
$countStmt->execute();
|
|
$totalCount = (int) $countStmt->fetchColumn();
|
|
|
|
$listSql = <<<'SQL'
|
|
SELECT
|
|
so.id,
|
|
so.external_ref,
|
|
so.order_date,
|
|
so.total_amount,
|
|
COALESCE(ad.first_name, '') AS first_name,
|
|
COALESCE(ad.last_name, '') AS last_name,
|
|
COALESCE(ad.street, '') AS street,
|
|
COALESCE(ad.house_number, '') AS house_number,
|
|
COALESCE(ad.zip, '') AS zip,
|
|
COALESCE(ad.city, '') AS city,
|
|
COALESCE(ad.country_name, '') AS country_name
|
|
SQL;
|
|
$listSql .= "\n" . $baseFromSql . "\n" . $filterSql;
|
|
$listSql .= "\nORDER BY {$sortExpression} {$sortDirection}, so.id {$sortDirection}";
|
|
$listSql .= "\nLIMIT :limit";
|
|
|
|
$listStmt = $pdo->prepare($listSql);
|
|
foreach ($params as $key => $value) {
|
|
$listStmt->bindValue($key, $value, PDO::PARAM_STR);
|
|
}
|
|
$listStmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
|
$listStmt->execute();
|
|
|
|
$rows = [];
|
|
foreach ($listStmt->fetchAll() as $row) {
|
|
$rows[] = [
|
|
'id' => (int) $row['id'],
|
|
'external_ref' => (string) $row['external_ref'],
|
|
'order_date' => (string) $row['order_date'],
|
|
'total_amount' => $row['total_amount'] !== null ? (float) $row['total_amount'] : null,
|
|
'first_name' => (string) $row['first_name'],
|
|
'last_name' => (string) $row['last_name'],
|
|
'street' => (string) $row['street'],
|
|
'house_number' => (string) $row['house_number'],
|
|
'zip' => (string) $row['zip'],
|
|
'city' => (string) $row['city'],
|
|
'country_name' => (string) $row['country_name'],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'rows' => $rows,
|
|
'search' => $searchTerm,
|
|
'sort_column' => $sortColumn,
|
|
'sort_direction' => $sortDirection,
|
|
'limit' => $limit,
|
|
'page_size' => $pageSize,
|
|
'total_count' => $totalCount,
|
|
'has_more' => $totalCount > $limit,
|
|
'next_limit' => min($totalCount, $limit + $pageSize),
|
|
];
|
|
}
|