batches->generate( quantity: $quantity, purpose: QrCodeBatch::PURPOSE_STORE, targetArea: null, targetStoreId: $store->id, createdBy: null, notes: "Wholesale to store {$store->business_name}", ); $now = now(); QrCode::query() ->where('batch_id', $batch->id) ->where('status', Unassigned::$name) ->update([ 'status' => Allocated::$name, 'assigned_to_store_id' => $store->id, 'allocated_at' => $now, 'updated_at' => $now, ]); $purchase = StorePurchase::create([ 'store_id' => $store->id, 'batch_id' => $batch->id, 'quantity' => $quantity, 'wholesale_price_centavos' => $wholesalePriceCentavos, 'paid_at' => $paidAt ?? $now, ]); $inventory = StoreInventory::firstOrCreate(['store_id' => $store->id], ['current_code_balance' => 0]); $inventory->forceFill([ 'current_code_balance' => $inventory->current_code_balance + $quantity, 'last_updated_at' => $now, ])->save(); return $purchase; }); } /** * Sell N codes from a store's inventory to a resident's household. * Codes transition allocated -> active and reassign household. */ public function sellToHousehold( PartnerStore $store, Household $household, int $quantity, int $retailPricePerCodeCentavos, ): StoreSale { return DB::transaction(function () use ($store, $household, $quantity, $retailPricePerCodeCentavos) { $codeIds = QrCode::query() ->where('assigned_to_store_id', $store->id) ->where('status', Allocated::$name) ->orderBy('id') ->limit($quantity) ->lockForUpdate() ->pluck('id'); if ($codeIds->count() < $quantity) { throw new \DomainException("Store has only {$codeIds->count()} codes available"); } $now = now(); QrCode::whereIn('id', $codeIds)->update([ 'status' => Active::$name, 'assigned_to_household_id' => $household->id, 'assigned_to_store_id' => null, 'activated_at' => $now, 'updated_at' => $now, ]); $totalRetail = $retailPricePerCodeCentavos * $quantity; $commission = (int) round($totalRetail * ($store->commission_rate_percent / 100)); $sale = StoreSale::create([ 'store_id' => $store->id, 'household_id' => $household->id, 'quantity' => $quantity, 'retail_price_centavos' => $totalRetail, 'commission_centavos' => $commission, 'sold_at' => $now, ]); $inventory = StoreInventory::firstOrCreate(['store_id' => $store->id], ['current_code_balance' => 0]); $inventory->forceFill([ 'current_code_balance' => max(0, $inventory->current_code_balance - $quantity), 'last_updated_at' => $now, ])->save(); // Notify the household head — fire after commit so the receiver // sees the persisted state. if ($household->head) { Notification::send( $household->head, new CodesPurchased($quantity, $totalRetail, $store->business_name), ); } return $sale; }); } }