loyaltyProgram(); } catch (\Throwable $e) { Log::debug('loyalty.program_unavailable', ['owner' => $ownerRef, 'error' => $e->getMessage()]); return null; } } public function isEnabled(string $ownerRef): bool { $program = $this->program($ownerRef); return (bool) ($program['enabled'] ?? false); } /** * @return array|null */ public function snapshot(string $ownerRef, int $crmCustomerId, int $amountMinor = 0): ?array { try { return CrmClient::for($ownerRef)->customerLoyalty($crmCustomerId, $amountMinor); } catch (\Throwable $e) { Log::debug('loyalty.snapshot_failed', [ 'owner' => $ownerRef, 'customer' => $crmCustomerId, 'error' => $e->getMessage(), ]); return null; } } /** * @return array|null */ public function quote(string $ownerRef, int $crmCustomerId, int $subtotalMinor, int $redeemPoints = 0): ?array { try { return CrmClient::for($ownerRef)->loyaltyQuote([ 'customer_id' => $crmCustomerId, 'subtotal_minor' => $subtotalMinor, 'redeem_points' => $redeemPoints, ]); } catch (\Throwable $e) { Log::debug('loyalty.quote_failed', ['error' => $e->getMessage()]); return null; } } /** * Apply redeem against CRM and return discount details for the sale. * * @return array{points: int, discount_minor: int, external_ref: string, balance: int} */ public function redeemForSale(PosSale $sale, int $redeemPoints): array { if (! $sale->crm_customer_id || $redeemPoints <= 0) { return ['points' => 0, 'discount_minor' => 0, 'external_ref' => '', 'balance' => 0]; } $externalRef = $sale->loyalty_external_ref ?: ('pos-sale-'.$sale->id.'-'.$sale->reference); try { $result = CrmClient::for($sale->owner_ref)->loyaltyRedeem([ 'customer_id' => (int) $sale->crm_customer_id, 'subtotal_minor' => (int) $sale->subtotal_minor, 'redeem_points' => $redeemPoints, 'external_ref' => $externalRef, 'source' => 'pos', 'description' => 'POS '.$sale->reference, ]); } catch (ValidationException $e) { throw new RuntimeException(collect($e->errors())->flatten()->first() ?: 'Could not redeem loyalty points.'); } catch (\Throwable $e) { throw new RuntimeException('Could not redeem loyalty points. Check CRM connectivity.'); } return [ 'points' => (int) ($result['points'] ?? 0), 'discount_minor' => (int) ($result['discount_minor'] ?? 0), 'external_ref' => $externalRef, 'balance' => (int) ($result['balance'] ?? 0), ]; } public function earnForSale(PosSale $sale): int { if (! $sale->crm_customer_id) { return 0; } $externalRef = $sale->loyalty_external_ref ?: ('pos-sale-'.$sale->id.'-'.$sale->reference); // Earn on net merchandise (exclude tip); tips are staff gratuity. $amount = max(0, (int) $sale->subtotal_minor - (int) $sale->loyalty_discount_minor); try { $result = CrmClient::for($sale->owner_ref)->loyaltyEarn([ 'customer_id' => (int) $sale->crm_customer_id, 'amount_minor' => $amount, 'external_ref' => $externalRef, 'source' => 'pos', 'description' => 'POS '.$sale->reference, ]); } catch (\Throwable $e) { Log::warning('loyalty.earn_failed', [ 'sale' => $sale->id, 'error' => $e->getMessage(), ]); return 0; } $points = (int) ($result['points'] ?? 0); if ($points > 0 || ! $sale->loyalty_points_earned) { $sale->forceFill([ 'loyalty_points_earned' => $points, 'loyalty_external_ref' => $externalRef, ])->save(); } return $points; } public function reverseForSale(PosSale $sale): void { $ref = $sale->loyalty_external_ref; if (! $ref && ($sale->loyalty_points_redeemed || $sale->loyalty_points_earned)) { $ref = 'pos-sale-'.$sale->id.'-'.$sale->reference; } if (! $ref) { return; } try { CrmClient::for($sale->owner_ref)->loyaltyReverse([ 'external_ref' => $ref, 'source' => 'pos', ]); } catch (\Throwable $e) { Log::warning('loyalty.reverse_failed', [ 'sale' => $sale->id, 'error' => $e->getMessage(), ]); } } /** * Payload for customer display. * * @return array|null */ public function displayPayload(?array $snapshot, int $redeemPoints = 0, int $earnPoints = 0): ?array { if (! $snapshot || ! ($snapshot['program']['enabled'] ?? $snapshot['points_balance'] ?? false)) { if (! $snapshot) { return null; } } if (! ($snapshot['program']['enabled'] ?? false)) { return null; } return [ 'label' => $snapshot['label'] ?? ($snapshot['program']['name'] ?? 'Rewards'), 'points_balance' => (int) ($snapshot['points_balance'] ?? 0), 'points_earn' => $earnPoints ?: (int) ($snapshot['points_earn'] ?? 0), 'points_redeem' => $redeemPoints, ]; } }