Wire POS register loyalty earn and redeem through CRM.
Deploy Ladill POS / deploy (push) Has been cancelled

Redeem at charge, earn on payment, reverse on cancel, and surface
balances on the till, customer display, sales, and receipts.
This commit is contained in:
isaacclad
2026-07-15 15:46:28 +00:00
parent 38b7f96714
commit 468346b183
12 changed files with 680 additions and 25 deletions
+33 -3
View File
@@ -19,11 +19,12 @@ class PosSaleService
public function __construct(
private MerchantGatewayService $gateway,
private PosTimelineService $timeline,
private PosLoyaltyService $loyalty,
) {}
/**
* @param list<array{product_id?: ?int, name: string, unit_price_minor: int, quantity: int}> $lines
* @param array{customer_name?: ?string, customer_email?: ?string, customer_phone?: ?string, crm_customer_id?: ?int, location_id?: ?int, currency?: string} $meta
* @param array{customer_name?: ?string, customer_email?: ?string, customer_phone?: ?string, crm_customer_id?: ?int, location_id?: ?int, currency?: string, loyalty_redeem_points?: int} $meta
*/
public function createSale(User $merchant, array $lines, array $meta = []): PosSale
{
@@ -50,6 +51,9 @@ class PosSaleService
'customer_phone' => $meta['customer_phone'] ?? null,
'crm_customer_id' => $meta['crm_customer_id'] ?? null,
'subtotal_minor' => $subtotal,
'loyalty_discount_minor' => 0,
'loyalty_points_redeemed' => 0,
'loyalty_points_earned' => 0,
'total_minor' => $subtotal,
'currency' => $currency,
]);
@@ -66,6 +70,28 @@ class PosSaleService
]);
}
$sale = $sale->fresh('lines');
$redeemPoints = (int) ($meta['loyalty_redeem_points'] ?? 0);
if ($redeemPoints > 0 && $sale->crm_customer_id) {
$sale = $this->applyLoyaltyRedeem($sale, $redeemPoints);
}
return $sale;
}
public function applyLoyaltyRedeem(PosSale $sale, int $redeemPoints): PosSale
{
$result = $this->loyalty->redeemForSale($sale, $redeemPoints);
$discount = (int) $result['discount_minor'];
$points = (int) $result['points'];
$sale->forceFill([
'loyalty_discount_minor' => $discount,
'loyalty_points_redeemed' => $points,
'loyalty_external_ref' => $result['external_ref'] ?: $sale->loyalty_external_ref,
'total_minor' => max(0, (int) $sale->subtotal_minor - $discount),
])->save();
return $sale->fresh('lines');
}
@@ -451,6 +477,8 @@ class PosSaleService
throw new RuntimeException('Only pending sales can be cancelled.');
}
$this->loyalty->reverseForSale($sale);
$sale->forceFill(['status' => PosSale::STATUS_CANCELLED])->save();
$this->closeTicket($sale);
}
@@ -553,9 +581,10 @@ class PosSaleService
$this->closeTicket($sale);
$sale = $sale->fresh('lines');
$this->loyalty->earnForSale($sale);
$this->timeline->pushPaidSale($sale);
return $sale;
return $sale->fresh('lines');
}
public function completePayCheckout(string $paymentReference): PosSale
@@ -579,9 +608,10 @@ class PosSaleService
$this->closeTicket($sale);
$sale = $sale->fresh('lines');
$this->loyalty->earnForSale($sale);
$this->timeline->pushPaidSale($sale);
return $sale;
return $sale->fresh('lines');
}
/**