$lines * @param array{customer_name?: ?string, customer_email?: ?string, customer_phone?: ?string, crm_customer_id?: ?int, location_id?: ?int, currency?: string} $meta */ public function createSale(User $merchant, array $lines, array $meta = []): PosSale { $normalized = $this->normalizeLines($lines); if ($normalized === []) { throw new RuntimeException('Add at least one item to the sale.'); } $subtotal = collect($normalized)->sum('line_total_minor'); if ($subtotal <= 0) { throw new RuntimeException('Sale total must be greater than zero.'); } $currency = strtoupper((string) ($meta['currency'] ?? config('pos.default_currency', 'GHS'))); $sale = PosSale::create([ 'owner_ref' => $merchant->public_id, 'location_id' => $meta['location_id'] ?? null, 'reference' => 'POS-'.strtoupper(Str::random(12)), 'status' => PosSale::STATUS_PENDING, 'payment_method' => PosSale::METHOD_PAY, 'customer_name' => $meta['customer_name'] ?? null, 'customer_email' => $meta['customer_email'] ?? null, 'customer_phone' => $meta['customer_phone'] ?? null, 'crm_customer_id' => $meta['crm_customer_id'] ?? null, 'subtotal_minor' => $subtotal, 'total_minor' => $subtotal, 'currency' => $currency, ]); foreach ($normalized as $index => $line) { PosSaleLine::create([ 'pos_sale_id' => $sale->id, 'product_id' => $line['product_id'] ?? null, 'name' => $line['name'], 'unit_price_minor' => $line['unit_price_minor'], 'quantity' => $line['quantity'], 'line_total_minor' => $line['line_total_minor'], 'position' => $index, ]); } return $sale->fresh('lines'); } /** * @return array{sale: PosSale, checkout_url: string} */ public function initiatePayCheckout(PosSale $sale, User $merchant): array { if ($sale->isPaid()) { throw new RuntimeException('This sale is already paid.'); } $sale->load('lines'); $callbackUrl = route('pos.sales.callback', $sale); $payOrder = $this->pay->createCheckout([ 'merchant' => $merchant->public_id, 'fee_tier' => 'sales', 'source_service' => 'pos', 'source_ref' => (string) $sale->id, 'callback_url' => $callbackUrl, 'customer_name' => $sale->customer_name, 'customer_email' => $sale->customer_email, 'customer_phone' => $sale->customer_phone, 'line_items' => $sale->lines->map(fn (PosSaleLine $line) => [ 'name' => $line->name, 'unit_price_minor' => $line->unit_price_minor, 'quantity' => $line->quantity, ])->all(), 'metadata' => [ 'pos_sale_id' => $sale->id, 'pos_reference' => $sale->reference, ], ]); $checkoutUrl = (string) ($payOrder['checkout_url'] ?? ''); if ($checkoutUrl === '') { throw new RuntimeException('Could not start checkout. Please try again.'); } $sale->forceFill([ 'payment_method' => PosSale::METHOD_PAY, 'pay_order_id' => $payOrder['id'] ?? null, 'payment_reference' => $payOrder['reference'] ?? null, ])->save(); return [ 'sale' => $sale->fresh('lines'), 'checkout_url' => $checkoutUrl, ]; } public function recordCashPayment(PosSale $sale): PosSale { if ($sale->isPaid()) { throw new RuntimeException('This sale is already paid.'); } $sale->forceFill([ 'payment_method' => PosSale::METHOD_CASH, 'status' => PosSale::STATUS_PAID, 'paid_at' => now(), ])->save(); $sale = $sale->fresh('lines'); $this->timeline->pushPaidSale($sale); return $sale; } public function completePayCheckout(string $paymentReference): PosSale { $sale = PosSale::query() ->where('payment_reference', $paymentReference) ->where('status', PosSale::STATUS_PENDING) ->firstOrFail(); $payOrder = $this->pay->verify($paymentReference); $sale->forceFill([ 'status' => PosSale::STATUS_PAID, 'total_minor' => (int) ($payOrder['amount_minor'] ?? $sale->total_minor), 'pay_order_id' => $payOrder['id'] ?? $sale->pay_order_id, 'paid_at' => now(), ])->save(); $sale = $sale->fresh('lines'); $this->timeline->pushPaidSale($sale); return $sale; } /** * @param list $lines * @return list */ private function normalizeLines(array $lines): array { $out = []; foreach ($lines as $line) { $qty = max(1, (int) ($line['quantity'] ?? 1)); $unit = max(0, (int) ($line['unit_price_minor'] ?? 0)); $name = trim((string) ($line['name'] ?? '')); if ($name === '' || $unit <= 0) { continue; } $out[] = [ 'product_id' => $line['product_id'] ?? null, 'name' => $name, 'unit_price_minor' => $unit, 'quantity' => $qty, 'line_total_minor' => $unit * $qty, ]; } return $out; } }