596 lines
24 KiB
PHP
596 lines
24 KiB
PHP
<?php
|
|
|
|
/**
|
|
* OrderManager
|
|
*
|
|
* Handles all read and write operations for sales orders (td_order)
|
|
* and their downstream stock-out effects on td_stock_<warehouse> tables.
|
|
*
|
|
* Method order:
|
|
* Transaction basis → getOrderList, getOrderById, generateOrderNumber,
|
|
* saveOrder, confirmOrder, cancelOrder
|
|
*
|
|
* Key design decisions:
|
|
* - Order items are stored as a JSON array in td_order.items.
|
|
* No separate child table exists. SKU-level reporting is served by
|
|
* td_stock_* rows (source='order') rather than querying items JSON.
|
|
* - confirmOrder() picks racks via FIFO — oldest approved stock-in row
|
|
* still rack-occupied, ordered by td_stock_<wh>.date ASC.
|
|
* - confirmOrder() creates stock-out rows with status=0 (draft).
|
|
* Warehouse staff approve them via the existing approve_stock.php
|
|
* engine, which handles rack release and balance adjustment.
|
|
* - cancelOrder() sets td_order.status = -1 and flips all linked
|
|
* draft stock-out rows (status=0) to status=-1 (cancelled).
|
|
* Approved stock-out rows block cancellation — caller must handle
|
|
* these manually before cancelling.
|
|
* - Cancel logic is intentionally self-contained here. status=-1 is
|
|
* an order-domain concept with no rack/balance side effects, so
|
|
* WarehouseManager and StockManager are not involved.
|
|
*
|
|
* Note: Write methods do NOT manage their own DB transactions.
|
|
* Callers must wrap multi-step operations inside dbTransaction().
|
|
*
|
|
* Security: All SQL uses PDO prepared statements with bound parameters.
|
|
* Dynamic table names (td_stock_<warehouse>) are sanitised with
|
|
* preg_replace('/[^a-zA-Z0-9_]/', '', ...) before interpolation —
|
|
* no user input ever reaches a table identifier directly.
|
|
*/
|
|
class OrderManager {
|
|
|
|
private $pdo;
|
|
private $company_id;
|
|
|
|
public function __construct($pdo, int $company_id) {
|
|
$this->pdo = $pdo;
|
|
$this->company_id = $company_id;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// Private helpers
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Build a standard audit log entry array for append-to-JSON log columns.
|
|
*
|
|
* @param string $action Short label e.g. 'create', 'update', 'confirm', 'cancel'.
|
|
* @return array
|
|
*/
|
|
private function buildLogEntry(string $action): array {
|
|
return [
|
|
'user_id' => $_SESSION['login_user_id'] ?? null,
|
|
'dt' => date('Y-m-d H:i:s'),
|
|
'login' => isset($_SESSION['otpTime'])
|
|
? date('Y-m-d H:i:s', $_SESSION['otpTime'])
|
|
: null,
|
|
'action' => $action,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Resolve the td_stock_<warehouse> table name for a warehouse_id.
|
|
*
|
|
* Does not filter by status — allows reading inactive warehouses
|
|
* for historical order lookups.
|
|
*
|
|
* @param int $warehouse_id
|
|
* @return string Sanitised table name e.g. "td_stock_Main".
|
|
* @throws Exception If warehouse not found.
|
|
*/
|
|
private function resolveStockTable(int $warehouse_id): string
|
|
{
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT warehouse_name FROM md_warehouse
|
|
WHERE company_id = :company_id AND id = :id"
|
|
);
|
|
$sth->execute([':company_id' => $this->company_id, ':id' => $warehouse_id]);
|
|
$name = $sth->fetchColumn();
|
|
|
|
if (!$name) {
|
|
throw new Exception("Warehouse ID {$warehouse_id} not found.");
|
|
}
|
|
|
|
$safe = preg_replace('/[^a-zA-Z0-9_]/', '', $name);
|
|
return "td_stock_{$safe}";
|
|
}
|
|
|
|
/**
|
|
* FIFO rack pick — find the oldest approved stock-in row for a SKU
|
|
* in a given warehouse that is still rack-occupied.
|
|
*
|
|
* "Oldest" = smallest date value among status=1 stock-in rows
|
|
* that have an md_rack row pointing back to them (td_stock_id IS set).
|
|
*
|
|
* @param int $warehouse_id
|
|
* @param string $product_sku
|
|
* @return array|null Full td_stock row + zone/aisle/rack from md_rack,
|
|
* or null if no available stock in this warehouse.
|
|
*/
|
|
private function pickFifoRack(int $warehouse_id, string $product_sku): ?array
|
|
{
|
|
$table = $this->resolveStockTable($warehouse_id);
|
|
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT s.*, r.zone, r.aisle, r.rack
|
|
FROM `{$table}` s
|
|
INNER JOIN md_rack r
|
|
ON r.company_id = s.company_id
|
|
AND r.warehouse = :warehouse_id
|
|
AND r.td_stock_id = s.id
|
|
AND r.product_sku IS NOT NULL
|
|
WHERE s.company_id = :company_id
|
|
AND s.product_sku = :product_sku
|
|
AND s.type = 'in'
|
|
AND s.status = 1
|
|
ORDER BY s.date ASC
|
|
LIMIT 1"
|
|
);
|
|
$sth->execute([
|
|
':company_id' => $this->company_id,
|
|
':warehouse_id' => $warehouse_id,
|
|
':product_sku' => $product_sku,
|
|
]);
|
|
|
|
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
|
return $row ?: null;
|
|
}
|
|
|
|
/**
|
|
* Generate the next sequential order number in ORD-YYYYMMDD-XXXX format.
|
|
*
|
|
* Finds the highest sequence number already used today and increments by 1.
|
|
* Thread-safe enough for single-company use; wrap in transaction if needed.
|
|
*
|
|
* @return string e.g. "ORD-20260502-0001"
|
|
*/
|
|
private function generateOrderNumber(): string
|
|
{
|
|
$prefix = 'ORD-' . date('Ymd') . '-';
|
|
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT order_number FROM td_order
|
|
WHERE company_id = :company_id
|
|
AND order_number LIKE :prefix
|
|
ORDER BY order_number DESC
|
|
LIMIT 1"
|
|
);
|
|
$sth->execute([
|
|
':company_id' => $this->company_id,
|
|
':prefix' => $prefix . '%',
|
|
]);
|
|
|
|
$last = $sth->fetchColumn();
|
|
$seq = $last ? ((int)substr($last, -4) + 1) : 1;
|
|
|
|
return $prefix . str_pad($seq, 4, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// TRANSACTION BASIS — Read
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Return all orders for the company, ordered by created_at DESC.
|
|
*
|
|
* Joins md_contact for contact_name display.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getOrderList(): array
|
|
{
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT o.*,
|
|
COALESCE(c.contact_name, '') AS contact_name
|
|
FROM td_order o
|
|
LEFT JOIN md_contact c
|
|
ON c.company_id = o.company_id
|
|
AND c.id = o.contact_id
|
|
WHERE o.company_id = :company_id
|
|
ORDER BY o.created_at DESC"
|
|
);
|
|
$sth->execute([':company_id' => $this->company_id]);
|
|
return $sth->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
/**
|
|
* Fetch a single order row by its primary key.
|
|
*
|
|
* Returns the row with items decoded as a PHP array.
|
|
*
|
|
* @param int $id td_order.id
|
|
* @return array|false
|
|
*/
|
|
public function getOrderById(int $id): array|false
|
|
{
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT o.*,
|
|
COALESCE(c.contact_name, '') AS contact_name
|
|
FROM td_order o
|
|
LEFT JOIN md_contact c
|
|
ON c.company_id = o.company_id
|
|
AND c.id = o.contact_id
|
|
WHERE o.company_id = :company_id
|
|
AND o.id = :id"
|
|
);
|
|
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
|
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) return false;
|
|
|
|
$row['items'] = json_decode($row['items'] ?? '[]', true) ?: [];
|
|
return $row;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
// TRANSACTION BASIS — Write
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Insert a new order (draft) or update metadata on an existing one.
|
|
*
|
|
* Insert flow (id = 0):
|
|
* - Generates order_number.
|
|
* - Stores items as JSON array.
|
|
* - Calculates grand_total from items + adjustments.
|
|
* - Sets status = 0 (draft), created_at = now().
|
|
*
|
|
* Update flow (id > 0):
|
|
* - Only allowed while status = 0 (draft).
|
|
* - Updates contact, date, items, totals, notes.
|
|
*
|
|
* Must be called inside dbTransaction() by the caller.
|
|
*
|
|
* @param array $data Keys: id, contact_id, order_date, items (array),
|
|
* discount, tax, shipping_fee, notes.
|
|
* @param array $logging Audit entry to append to log column.
|
|
* @return int New td_order.id on insert, 0 on update.
|
|
* @throws Exception If updating a non-draft order.
|
|
*/
|
|
public function saveOrder(array $data, array $logging): int
|
|
{
|
|
$id = (int)($data['id'] ?? 0);
|
|
$items = $data['items'] ?? [];
|
|
|
|
// Calculate totals from items
|
|
$subtotal = array_reduce($items, fn($carry, $item) =>
|
|
$carry + (float)($item['total_price'] ?? 0), 0.0
|
|
);
|
|
$discount = (float)($data['discount'] ?? 0);
|
|
$tax = (float)($data['tax'] ?? 0);
|
|
$shipping_fee = (float)($data['shipping_fee'] ?? 0);
|
|
$grand_total = $subtotal - $discount + $tax + $shipping_fee;
|
|
|
|
if ($id > 0) {
|
|
|
|
// Fetch existing row to check status and load log
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT status, `log` FROM td_order
|
|
WHERE company_id = :company_id AND id = :id"
|
|
);
|
|
$sth->execute([':company_id' => $this->company_id, ':id' => $id]);
|
|
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
throw new Exception("Order not found.");
|
|
}
|
|
if ((int)$row['status'] !== 0) {
|
|
throw new Exception("Only draft orders can be edited.");
|
|
}
|
|
|
|
$log = json_decode($row['log'] ?? '[]', true) ?: [];
|
|
$log[] = $logging;
|
|
|
|
$this->pdo->prepare(
|
|
"UPDATE td_order SET
|
|
contact_id = :contact_id,
|
|
order_date = :order_date,
|
|
items = :items,
|
|
subtotal = :subtotal,
|
|
discount = :discount,
|
|
tax = :tax,
|
|
shipping_fee = :shipping_fee,
|
|
grand_total = :grand_total,
|
|
notes = :notes,
|
|
`log` = :log
|
|
WHERE id = :id AND company_id = :company_id"
|
|
)->execute([
|
|
':contact_id' => (int)($data['contact_id'] ?? 0),
|
|
':order_date' => $data['order_date'] ?? date('Y-m-d'),
|
|
':items' => json_encode($items, JSON_UNESCAPED_UNICODE),
|
|
':subtotal' => $subtotal,
|
|
':discount' => $discount,
|
|
':tax' => $tax,
|
|
':shipping_fee' => $shipping_fee,
|
|
':grand_total' => $grand_total,
|
|
':notes' => $data['notes'] ?? '',
|
|
':log' => json_encode($log),
|
|
':id' => $id,
|
|
':company_id' => $this->company_id,
|
|
]);
|
|
|
|
return 0;
|
|
|
|
} else {
|
|
|
|
$log = [$logging];
|
|
|
|
$this->pdo->prepare(
|
|
"INSERT INTO td_order
|
|
(company_id, uuid, order_number, contact_id, order_date,
|
|
status, payment_status, subtotal, discount, tax,
|
|
shipping_fee, grand_total, items, notes, `log`, created_at)
|
|
VALUES
|
|
(:company_id, :uuid, :order_number, :contact_id, :order_date,
|
|
0, 0, :subtotal, :discount, :tax,
|
|
:shipping_fee, :grand_total, :items, :notes, :log, :created_at)"
|
|
)->execute([
|
|
':company_id' => $this->company_id,
|
|
':uuid' => bin2hex(random_bytes(16)),
|
|
':order_number' => $this->generateOrderNumber(),
|
|
':contact_id' => (int)($data['contact_id'] ?? 0),
|
|
':order_date' => $data['order_date'] ?? date('Y-m-d'),
|
|
':subtotal' => $subtotal,
|
|
':discount' => $discount,
|
|
':tax' => $tax,
|
|
':shipping_fee' => $shipping_fee,
|
|
':grand_total' => $grand_total,
|
|
':items' => json_encode($items, JSON_UNESCAPED_UNICODE),
|
|
':notes' => $data['notes'] ?? '',
|
|
':log' => json_encode($log),
|
|
':created_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
|
|
return (int)$this->pdo->lastInsertId();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Confirm a draft order — create stock-out rows (status=0) for each item
|
|
* using FIFO rack selection, then advance the order to status=1.
|
|
*
|
|
* Flow per item:
|
|
* 1. pickFifoRack() — find oldest rack-occupied stock-in row for the SKU.
|
|
* 2. INSERT into td_stock_<wh> with type='out', status=0,
|
|
* source='order', source_id=order_id.
|
|
* 3. Write back stock_out_id into the item object in td_order.items.
|
|
*
|
|
* After all items are processed:
|
|
* 4. UPDATE td_order.items with stock_out_id values written back.
|
|
* 5. UPDATE td_order.status = 1 (confirmed).
|
|
*
|
|
* Rack release and balance adjustment are intentionally deferred —
|
|
* they happen when warehouse staff approve the stock-out rows via
|
|
* the existing approve_stock.php engine (StockManager::approveStock).
|
|
*
|
|
* Must be called inside dbTransaction() by the caller.
|
|
*
|
|
* @param int $order_id td_order.id to confirm.
|
|
* @param string $uuid UUID prefix — each stock-out row gets uuid_{$i}.
|
|
* @param array $logging Audit entry appended to td_order.log.
|
|
* @throws Exception If order not found, not in draft status, or any item
|
|
* has no available FIFO rack in its assigned warehouse.
|
|
*/
|
|
public function confirmOrder(int $order_id, string $uuid, array $logging, bool $auto_approve = false): void
|
|
{
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT * FROM td_order
|
|
WHERE company_id = :company_id AND id = :id"
|
|
);
|
|
$sth->execute([':company_id' => $this->company_id, ':id' => $order_id]);
|
|
$order = $sth->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$order) {
|
|
throw new Exception("Order not found.");
|
|
}
|
|
if ((int)$order['status'] !== 0) {
|
|
throw new Exception("Only draft orders can be confirmed.");
|
|
}
|
|
|
|
$items = json_decode($order['items'] ?? '[]', true) ?: [];
|
|
|
|
if (empty($items)) {
|
|
throw new Exception("Cannot confirm an order with no items.");
|
|
}
|
|
|
|
// ── Per-item: FIFO pick + insert stock-out row ────────────────────
|
|
foreach ($items as $i => &$item) {
|
|
|
|
$warehouse_id = (int)($item['warehouse_id'] ?? 0);
|
|
$product_sku = $item['product_sku'] ?? '';
|
|
|
|
if (!$warehouse_id || !$product_sku) {
|
|
throw new Exception(
|
|
"Item #{$i}: missing warehouse_id or product_sku."
|
|
);
|
|
}
|
|
|
|
$rack_stock = $this->pickFifoRack($warehouse_id, $product_sku);
|
|
|
|
if (!$rack_stock) {
|
|
$name = $item['product_name'] ?? $product_sku;
|
|
throw new Exception(
|
|
"No available stock for \"{$name}\" in the selected warehouse."
|
|
);
|
|
}
|
|
|
|
$table = $this->resolveStockTable($warehouse_id);
|
|
$item_uuid = $uuid . '_' . $i;
|
|
$item_log = [array_merge($logging, ['action' => 'confirm_item'])];
|
|
|
|
$this->pdo->prepare(
|
|
"INSERT INTO `{$table}`
|
|
(uuid, company_id, `date`, product_sku, `out`,
|
|
zone, aisle, rack,
|
|
contact_id, `description`, `log`, `type`,
|
|
ref_id, lot_number, serial_number,
|
|
source, source_id, price, status)
|
|
VALUES
|
|
(:uuid, :company_id, :date, :product_sku, :quantity,
|
|
:zone, :aisle, :rack,
|
|
:contact_id, :description, :log, 'out',
|
|
:ref_id, :lot_number, :serial_number,
|
|
'order', :source_id, :price, 0)"
|
|
)->execute([
|
|
':uuid' => $item_uuid,
|
|
':company_id' => $this->company_id,
|
|
':date' => date('Y-m-d H:i:s'),
|
|
':product_sku' => $product_sku,
|
|
':quantity' => (float)$item['quantity'],
|
|
':zone' => $rack_stock['zone'],
|
|
':aisle' => $rack_stock['aisle'],
|
|
':rack' => $rack_stock['rack'],
|
|
':contact_id' => (int)$order['contact_id'],
|
|
':description' => $order['order_number'],
|
|
':log' => json_encode($item_log),
|
|
':ref_id' => (int)$rack_stock['id'],
|
|
':lot_number' => $rack_stock['lot_number'] ?? '',
|
|
':serial_number' => $rack_stock['serial_number'] ?? '',
|
|
':source_id' => $order_id,
|
|
':price' => (float)($item['price'] ?? 0),
|
|
]);
|
|
|
|
// Write rack context + stock_out_id back into the item object
|
|
$item['zone'] = $rack_stock['zone'];
|
|
$item['aisle'] = $rack_stock['aisle'];
|
|
$item['rack'] = $rack_stock['rack'];
|
|
$item['lot_number'] = $rack_stock['lot_number'] ?? '';
|
|
$item['serial_number'] = $rack_stock['serial_number'] ?? '';
|
|
$item['stock_out_id'] = (int)$this->pdo->lastInsertId();
|
|
|
|
// Auto-approve: immediately release rack + adjust balance
|
|
if ($auto_approve) {
|
|
$stock = new StockManager($this->pdo, $this->company_id);
|
|
$whMgmt = new WarehouseManager($this->pdo, $this->company_id);
|
|
$stock->approveStock($item['stock_out_id'], $warehouse_id, 'out', $whMgmt);
|
|
}
|
|
}
|
|
unset($item); // break reference
|
|
|
|
// ── Update order: items (with stock_out_id) + status=1 ───────────
|
|
$log = json_decode($order['log'] ?? '[]', true) ?: [];
|
|
$log[] = array_merge($logging, ['action' => 'confirm']);
|
|
|
|
$this->pdo->prepare(
|
|
"UPDATE td_order SET
|
|
status = 1,
|
|
items = :items,
|
|
`log` = :log
|
|
WHERE id = :id AND company_id = :company_id"
|
|
)->execute([
|
|
':items' => json_encode($items, JSON_UNESCAPED_UNICODE),
|
|
':log' => json_encode($log),
|
|
':id' => $order_id,
|
|
':company_id' => $this->company_id,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Cancel an order and flip all linked draft stock-out rows to status=-1.
|
|
*
|
|
* Flow:
|
|
* 1. Load the order — must be status=0 (draft) or status=1 (confirmed).
|
|
* Orders at status=2 (processing) or status=3 (completed) cannot be
|
|
* cancelled here; they require manual stock reversal first.
|
|
* 2. Check that no linked stock-out rows are already approved (status=1).
|
|
* If any are approved, throw — those must be deleted via the existing
|
|
* deleteStockOut() flow before cancellation can proceed.
|
|
* 3. UPDATE all td_stock_<wh> rows where
|
|
* source='order' AND source_id=order_id AND status=0 → status=-1.
|
|
* 4. UPDATE td_order.status = -1.
|
|
*
|
|
* Cancel logic is intentionally self-contained — status=-1 rows have no
|
|
* rack/balance side effects so WarehouseManager is not involved.
|
|
*
|
|
* Must be called inside dbTransaction() by the caller.
|
|
*
|
|
* @param int $order_id td_order.id to cancel.
|
|
* @param array $logging Audit entry appended to td_order.log.
|
|
* @throws Exception If order not found, already cancelled, in a non-cancellable
|
|
* status, or has approved stock-out rows.
|
|
*/
|
|
public function cancelOrder(int $order_id, array $logging): void
|
|
{
|
|
// ── Load order ────────────────────────────────────────────────────
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT * FROM td_order
|
|
WHERE company_id = :company_id AND id = :id"
|
|
);
|
|
$sth->execute([':company_id' => $this->company_id, ':id' => $order_id]);
|
|
$order = $sth->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$order) {
|
|
throw new Exception("Order not found.");
|
|
}
|
|
|
|
$status = (int)$order['status'];
|
|
|
|
if ($status === -1) {
|
|
throw new Exception("Order is already cancelled.");
|
|
}
|
|
if ($status >= 2) {
|
|
throw new Exception(
|
|
"Cannot cancel an order that is processing or completed. " .
|
|
"Please reverse stock movements manually first."
|
|
);
|
|
}
|
|
|
|
// ── Guard: no approved stock-out rows ─────────────────────────────
|
|
// Discover all td_stock_* tables and check for approved rows
|
|
// linked to this order before making any changes.
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT table_name FROM information_schema.tables
|
|
WHERE table_schema = DATABASE()
|
|
AND table_name LIKE 'td_stock_%'"
|
|
);
|
|
$sth->execute();
|
|
$tables = $sth->fetchAll(PDO::FETCH_COLUMN);
|
|
|
|
foreach ($tables as $table) {
|
|
$sth = $this->pdo->prepare(
|
|
"SELECT COUNT(*) FROM `{$table}`
|
|
WHERE company_id = :company_id
|
|
AND source = 'order'
|
|
AND source_id = :order_id
|
|
AND status = 1"
|
|
);
|
|
$sth->execute([
|
|
':company_id' => $this->company_id,
|
|
':order_id' => $order_id,
|
|
]);
|
|
if ((int)$sth->fetchColumn() > 0) {
|
|
throw new Exception(
|
|
"Cannot cancel — some stock-out rows for this order are already " .
|
|
"approved. Please delete them from the Stock Out page first."
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Flip draft stock-out rows to cancelled ────────────────────────
|
|
foreach ($tables as $table) {
|
|
$this->pdo->prepare(
|
|
"UPDATE `{$table}` SET status = -1
|
|
WHERE company_id = :company_id
|
|
AND source = 'order'
|
|
AND source_id = :order_id
|
|
AND status = 0"
|
|
)->execute([
|
|
':company_id' => $this->company_id,
|
|
':order_id' => $order_id,
|
|
]);
|
|
}
|
|
|
|
// ── Cancel the order ──────────────────────────────────────────────
|
|
$log = json_decode($order['log'] ?? '[]', true) ?: [];
|
|
$log[] = array_merge($logging, ['action' => 'cancel']);
|
|
|
|
$this->pdo->prepare(
|
|
"UPDATE td_order SET
|
|
status = -1,
|
|
`log` = :log
|
|
WHERE id = :id AND company_id = :company_id"
|
|
)->execute([
|
|
':log' => json_encode($log),
|
|
':id' => $order_id,
|
|
':company_id' => $this->company_id,
|
|
]);
|
|
}
|
|
} |