取貨碼/通行碼/銷售紀錄 後台功能
- 取貨碼使用紀錄記實際出貨貨道(PickupCodeLog.raw_data.slot) - 銷售紀錄訂單列(支付金額欄)顯示取貨碼序號(member_barcode) - 遠端指令中心「遠端出貨」→「遠端開櫃」(zh_TW.json Remote Dispense) - 訂單品項記消費者選的貨道(order_items.slot_no + migration),失敗交易也看得到; 訂單明細顯示 Slot;OrderItem/TransactionService 對應 - 通行碼「只能使用一次」:pass_codes.usage_limit/usage_count + migration; 建立表單加勾選;B670(verifyPassCode)驗證時消耗、達上限標 used;PassCode.isValid 判斷 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ce9e52efd9
commit
dc9421e482
@ -841,6 +841,8 @@ class SalesController extends Controller
|
|||||||
$machine = Machine::findOrFail($validated['machine_id']);
|
$machine = Machine::findOrFail($validated['machine_id']);
|
||||||
$quantity = (int) ($validated['quantity'] ?? 1);
|
$quantity = (int) ($validated['quantity'] ?? 1);
|
||||||
$expiresAt = $request->expires_days ? now()->addDays((int) $request->expires_days) : null;
|
$expiresAt = $request->expires_days ? now()->addDays((int) $request->expires_days) : null;
|
||||||
|
// 勾「只能使用一次」→ 使用上限 1;未勾 → null(無限次,維持原行為)。
|
||||||
|
$usageLimit = $request->boolean('single_use') ? 1 : null;
|
||||||
|
|
||||||
// 批次產生(>1 筆):忽略自訂碼,系統自動產生隨機唯一碼,並以 batch_no 標記同一批。
|
// 批次產生(>1 筆):忽略自訂碼,系統自動產生隨機唯一碼,並以 batch_no 標記同一批。
|
||||||
if ($quantity > 1) {
|
if ($quantity > 1) {
|
||||||
@ -859,6 +861,8 @@ class SalesController extends Controller
|
|||||||
'batch_no' => $batchNo,
|
'batch_no' => $batchNo,
|
||||||
'expires_at' => $expiresAt,
|
'expires_at' => $expiresAt,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
|
'usage_limit' => $usageLimit,
|
||||||
|
'usage_count' => 0,
|
||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
'created_at' => $nowTs,
|
'created_at' => $nowTs,
|
||||||
'updated_at' => $nowTs,
|
'updated_at' => $nowTs,
|
||||||
@ -902,6 +906,8 @@ class SalesController extends Controller
|
|||||||
'code' => $request->custom_code ?: PassCode::generateUniqueCode($validated['machine_id']),
|
'code' => $request->custom_code ?: PassCode::generateUniqueCode($validated['machine_id']),
|
||||||
'expires_at' => $expiresAt,
|
'expires_at' => $expiresAt,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
|
'usage_limit' => $usageLimit,
|
||||||
|
'usage_count' => 0,
|
||||||
'company_id' => $machine->company_id,
|
'company_id' => $machine->company_id,
|
||||||
'created_by' => Auth::id(),
|
'created_by' => Auth::id(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -852,6 +852,17 @@ class MachineController extends Controller
|
|||||||
'raw_data' => ['machine_sn' => $machine->serial_no, 'code' => $passCode->code]
|
'raw_data' => ['machine_sn' => $machine->serial_no, 'code' => $passCode->code]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 單次/限次通行碼:驗證成功即計為使用一次;達上限就標記 used,
|
||||||
|
// 下次驗證會被上方 where('status','active') 過濾擋掉,達成「只能使用一次」。
|
||||||
|
// usage_limit 為 null(未勾單次)時不計數,維持原本無限次行為。
|
||||||
|
if (!is_null($passCode->usage_limit)) {
|
||||||
|
$newCount = $passCode->usage_count + 1;
|
||||||
|
$passCode->update([
|
||||||
|
'usage_count' => $newCount,
|
||||||
|
'status' => $newCount >= $passCode->usage_limit ? 'used' : $passCode->status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ class OrderItem extends Model
|
|||||||
'order_id',
|
'order_id',
|
||||||
'product_id',
|
'product_id',
|
||||||
'product_name',
|
'product_name',
|
||||||
|
'slot_no',
|
||||||
'barcode',
|
'barcode',
|
||||||
'price',
|
'price',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
|||||||
@ -21,6 +21,8 @@ class PassCode extends Model
|
|||||||
'batch_no',
|
'batch_no',
|
||||||
'expires_at',
|
'expires_at',
|
||||||
'status',
|
'status',
|
||||||
|
'usage_limit', // 可用次數上限;null=無限次。勾「只能使用一次」時=1
|
||||||
|
'usage_count', // 已使用次數
|
||||||
'created_by',
|
'created_by',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -77,6 +79,11 @@ class PassCode extends Model
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 有設使用次數上限(如「只能使用一次」usage_limit=1)且已用完 → 失效。null=無限次。
|
||||||
|
if (!is_null($this->usage_limit) && $this->usage_count >= $this->usage_limit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -158,6 +158,8 @@ class TransactionService
|
|||||||
$order->items()->create([
|
$order->items()->create([
|
||||||
'product_id' => $item['product_id'],
|
'product_id' => $item['product_id'],
|
||||||
'product_name' => $productName,
|
'product_name' => $productName,
|
||||||
|
// 消費者選擇的貨道/格子號(App items[].slot_no 帶入);不管出貨成功或失敗都記錄。
|
||||||
|
'slot_no' => $item['slot_no'] ?? null,
|
||||||
'barcode' => $item['barcode'] ?? null,
|
'barcode' => $item['barcode'] ?? null,
|
||||||
'price' => $item['price'],
|
'price' => $item['price'],
|
||||||
'quantity' => $item['quantity'],
|
'quantity' => $item['quantity'],
|
||||||
@ -460,6 +462,7 @@ class TransactionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Record Dispense Results (B602) - Optional/Multiple
|
// 3. Record Dispense Results (B602) - Optional/Multiple
|
||||||
|
$dispensedSlots = []; // 收集實際出貨貨道/櫃號,供取貨碼核銷日誌記錄「真正取貨的貨道」
|
||||||
if (!empty($data['dispense'])) {
|
if (!empty($data['dispense'])) {
|
||||||
$dispenseList = isset($data['dispense'][0]) ? $data['dispense'] : [$data['dispense']];
|
$dispenseList = isset($data['dispense'][0]) ? $data['dispense'] : [$data['dispense']];
|
||||||
foreach ($dispenseList as $dispenseItem) {
|
foreach ($dispenseList as $dispenseItem) {
|
||||||
@ -472,6 +475,10 @@ class TransactionService
|
|||||||
$dispenseItem['member_barcode'] = $order->member_barcode;
|
$dispenseItem['member_barcode'] = $order->member_barcode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($dispenseItem['slot_no'])) {
|
||||||
|
$dispensedSlots[] = (string) $dispenseItem['slot_no'];
|
||||||
|
}
|
||||||
|
|
||||||
$this->recordDispense($dispenseItem);
|
$this->recordDispense($dispenseItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -523,7 +530,11 @@ class TransactionService
|
|||||||
'action' => $isUsedUp ? 'used' : 'consume',
|
'action' => $isUsedUp ? 'used' : 'consume',
|
||||||
'remark' => "MQTT 交易完成自動核銷 (" . ($newCount) . "/" . $pickupCode->usage_limit . "),訂單: #{$order->id}",
|
'remark' => "MQTT 交易完成自動核銷 (" . ($newCount) . "/" . $pickupCode->usage_limit . "),訂單: #{$order->id}",
|
||||||
'raw_data' => [
|
'raw_data' => [
|
||||||
'slot' => $pickupCode->slot_no,
|
// 取貨碼綁「商品」時 pickupCode->slot_no 為 null;
|
||||||
|
// 改記「實際出貨的貨道/櫃號」(dispense),取不到才退回取貨碼綁定貨道。
|
||||||
|
'slot' => !empty($dispensedSlots)
|
||||||
|
? implode(', ', array_unique($dispensedSlots))
|
||||||
|
: $pickupCode->slot_no,
|
||||||
'code' => $pickupCode->code,
|
'code' => $pickupCode->code,
|
||||||
'count' => $newCount,
|
'count' => $newCount,
|
||||||
'limit' => $pickupCode->usage_limit
|
'limit' => $pickupCode->usage_limit
|
||||||
@ -533,6 +544,8 @@ class TransactionService
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 5: // 通行碼 (Pass Code)
|
case 5: // 通行碼 (Pass Code)
|
||||||
|
// 注意:通行碼「用掉一次」的計數改在 B670 驗證(verifyPassCode)當下處理,
|
||||||
|
// 因為 App 對通行碼會略過交易結案上報,此 case 5 實務上不會被觸發。
|
||||||
// 建立通行碼核銷日誌
|
// 建立通行碼核銷日誌
|
||||||
PassCodeLog::create([
|
PassCodeLog::create([
|
||||||
'company_id' => $order->company_id,
|
'company_id' => $order->company_id,
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('order_items', function (Blueprint $table) {
|
||||||
|
// 消費者選擇的貨道/格子號(格子櫃=櫃號如 501,VMC=貨道號)。
|
||||||
|
// 由 App transaction finalize 的 items[].slot_no 帶入;不管出貨成功或失敗都記錄。
|
||||||
|
$table->string('slot_no', 20)->nullable()->after('product_name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('order_items', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('slot_no');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('pass_codes', function (Blueprint $table) {
|
||||||
|
// 可用次數上限;null=無限次(維持原行為)。勾「只能使用一次」時=1。
|
||||||
|
$table->unsignedInteger('usage_limit')->nullable()->after('status');
|
||||||
|
$table->unsignedInteger('usage_count')->default(0)->after('usage_limit');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('pass_codes', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['usage_limit', 'usage_count']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"Single use only": "只能使用一次",
|
||||||
"Search Product...": "搜尋商品...",
|
"Search Product...": "搜尋商品...",
|
||||||
"Generate Quantity": "產生數量",
|
"Generate Quantity": "產生數量",
|
||||||
"Batch mode: codes are auto-generated and the custom code is ignored.": "批次模式:系統自動產生隨機碼,將忽略自訂碼。",
|
"Batch mode: codes are auto-generated and the custom code is ignored.": "批次模式:系統自動產生隨機碼,將忽略自訂碼。",
|
||||||
@ -1806,7 +1807,7 @@
|
|||||||
"Remote Change": "遠端找零",
|
"Remote Change": "遠端找零",
|
||||||
"Remote Checkout": "遠端結帳",
|
"Remote Checkout": "遠端結帳",
|
||||||
"Remote Command Center": "遠端指令中心",
|
"Remote Command Center": "遠端指令中心",
|
||||||
"Remote Dispense": "遠端出貨",
|
"Remote Dispense": "遠端開櫃",
|
||||||
"Remote Lock": "遠端鎖定",
|
"Remote Lock": "遠端鎖定",
|
||||||
"Remote Management": "遠端管理",
|
"Remote Management": "遠端管理",
|
||||||
"Remote Permissions": "遠端管理權限",
|
"Remote Permissions": "遠端管理權限",
|
||||||
|
|||||||
@ -233,6 +233,10 @@
|
|||||||
<p class="text-base font-extrabold text-slate-800 dark:text-white truncate tracking-tight">{{ $item->product_name }}</p>
|
<p class="text-base font-extrabold text-slate-800 dark:text-white truncate tracking-tight">{{ $item->product_name }}</p>
|
||||||
<div class="flex items-center gap-3 mt-1">
|
<div class="flex items-center gap-3 mt-1">
|
||||||
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">${{ number_format($item->price, 0) }} × {{ $item->quantity }}</span>
|
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest">${{ number_format($item->price, 0) }} × {{ $item->quantity }}</span>
|
||||||
|
@if(!empty($item->slot_no))
|
||||||
|
{{-- 消費者選擇的貨道/格子(不管出貨成功失敗都會記錄) --}}
|
||||||
|
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 uppercase tracking-widest">{{ __('Slot') }}: {{ $item->slot_no }}</span>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
|
|||||||
@ -285,6 +285,13 @@
|
|||||||
{{ __('Discounted') }} ${{ number_format($order->discount_amount, 0) }}
|
{{ __('Discounted') }} ${{ number_format($order->discount_amount, 0) }}
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
|
@if((int) $order->payment_type === 6 && !empty($order->member_barcode))
|
||||||
|
{{-- 取貨碼交易:於支付金額欄換行加註取貨碼序號 --}}
|
||||||
|
<span class="text-[10px] font-black text-cyan-600 dark:text-cyan-400 mt-1.5 flex items-center gap-1">
|
||||||
|
{{ __('Pickup Code') }}:
|
||||||
|
<span class="font-mono tracking-tighter normal-case text-slate-700 dark:text-slate-200">{{ $order->member_barcode }}</span>
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
@if($order->payment_type == 41 && $order->staffCardLog && $order->staffCardLog->staffCard)
|
@if($order->payment_type == 41 && $order->staffCardLog && $order->staffCardLog->staffCard)
|
||||||
<span class="text-[10px] text-cyan-600 dark:text-cyan-400 font-extrabold mt-1.5 flex items-center gap-1">
|
<span class="text-[10px] text-cyan-600 dark:text-cyan-400 font-extrabold mt-1.5 flex items-center gap-1">
|
||||||
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
|||||||
@ -421,6 +421,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- 只能使用一次:勾選 → usage_limit=1;不勾 → 無限次(維持原行為) --}}
|
||||||
|
<label class="flex items-center gap-3 px-1 cursor-pointer select-none">
|
||||||
|
<input type="checkbox" name="single_use" value="1"
|
||||||
|
class="w-5 h-5 rounded border-slate-300 dark:border-slate-600 text-cyan-500 focus:ring-cyan-500 cursor-pointer">
|
||||||
|
<span class="text-xs font-black text-slate-500 uppercase tracking-widest">{{ __('Single use only') }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="flex justify-end gap-x-4 pt-8">
|
<div class="flex justify-end gap-x-4 pt-8">
|
||||||
<button type="button" @click="showCreateModal = false" class="btn-luxury-ghost px-8">
|
<button type="button" @click="showCreateModal = false" class="btn-luxury-ghost px-8">
|
||||||
{{ __('Cancel') }}
|
{{ __('Cancel') }}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user