[FEAT] 綠界電子發票設定整組選填一旦啟用即全欄必填

1. PaymentConfigController 的 store/update 新增 ecpay_invoice 群組條件式驗證:四欄全空時整組選填,只要填寫任一欄(store_id/hash_key/hash_iv/email)即透過 required_with 強制其餘全部必填。
2. email 欄位加上格式驗證,從源頭避免空值導致綠界開立失敗(綠界規定 CustomerPhone 與 CustomerEmail 至少擇一,本系統 email 為唯一來源)。
3. 新增三語系驗證訊息與表單提示 key(zh_TW/en/ja),維持字母排序與斜線不逸出。
4. create/edit 表單加入動態必填提示:填入任一欄時 label 顯示紅色星號,並於各欄下方顯示 @error 驗證訊息。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-16 10:30:06 +08:00
parent d3e455fc82
commit e81756833c
6 changed files with 65 additions and 20 deletions

View File

@ -51,7 +51,7 @@ class PaymentConfigController extends AdminController
'name' => 'required|string|max:255',
'company_id' => 'nullable|exists:companies,id',
'settings' => 'required|array',
]);
] + $this->ecpayInvoiceRules(), $this->ecpayInvoiceMessages());
PaymentConfig::create([
'name' => $request->name,
@ -83,7 +83,7 @@ class PaymentConfigController extends AdminController
'name' => 'required|string|max:255',
'company_id' => 'nullable|exists:companies,id',
'settings' => 'required|array',
]);
] + $this->ecpayInvoiceRules(), $this->ecpayInvoiceMessages());
$paymentConfig->update([
'name' => $request->name,
@ -105,4 +105,30 @@ class PaymentConfigController extends AdminController
return redirect()->route('admin.basic-settings.payment-configs.index')
->with('success', __('Payment Configuration deleted successfully.'));
}
/**
* 綠界電子發票群組驗證規則:整組選填,但只要填了任一欄,
* store_id / hash_key / hash_iv / email 即全部必填(綠界開立發票需金鑰齊備,
* email 為「電話/信箱擇一」的唯一來源,缺一即無法開立)。
* 依賴 ConvertEmptyStringsToNull middleware空欄送出為 nullrequired_with 不會誤觸發。
*/
private function ecpayInvoiceRules(): array
{
$g = 'settings.ecpay_invoice';
return [
"{$g}.store_id" => "nullable|string|max:20|required_with:{$g}.hash_key,{$g}.hash_iv,{$g}.email",
"{$g}.hash_key" => "nullable|string|max:64|required_with:{$g}.store_id,{$g}.hash_iv,{$g}.email",
"{$g}.hash_iv" => "nullable|string|max:64|required_with:{$g}.store_id,{$g}.hash_key,{$g}.email",
"{$g}.email" => "nullable|email|max:100|required_with:{$g}.store_id,{$g}.hash_key,{$g}.hash_iv",
];
}
/** 綠界電子發票群組的驗證訊息(三語系)。 */
private function ecpayInvoiceMessages(): array
{
return [
'settings.ecpay_invoice.*.required_with' => __('ECPay e-invoice is optional, but once any field is filled, Store ID / Hash Key / Hash IV / Email are all required.'),
'settings.ecpay_invoice.email.email' => __('Please enter a valid ECPay e-invoice notification email.'),
];
}
}

View File

@ -609,6 +609,7 @@
"EASY_MERCHANT_ID": "EASY_MERCHANT_ID (EasyWallet)",
"ECPay Invoice": "ECPay Invoice",
"ECPay Invoice Settings Description": "ECPay Invoice Settings Description",
"ECPay e-invoice is optional, but once any field is filled, Store ID / Hash Key / Hash IV / Email are all required.": "ECPay e-invoice is optional, but once any field is filled, Store ID / Hash Key / Hash IV / Email are all required.",
"ECPay has no such invoice; marked as pending re-issue": "ECPay has no such invoice; marked as pending re-issue",
"ECPay query failed": "ECPay query failed",
"ESUN": "ESUN",
@ -1404,6 +1405,7 @@
"Optional notes...": "Optional notes...",
"Optional remarks": "Optional remarks",
"Optional remarks...": "Optional remarks...",
"Optional. If you fill in any field, all fields below become required.": "Optional. If you fill in any field, all fields below become required.",
"Or Choose Custom Date": "Or Choose Custom Date",
"Order Details": "Order Details",
"Order Info": "Order Info",
@ -1536,6 +1538,7 @@
"Please check the form for errors.": "Please check the form for errors.",
"Please confirm the details below": "Please confirm the details below",
"Please enter Webhook URL first.": "Please enter Webhook URL first.",
"Please enter a valid ECPay e-invoice notification email.": "Please enter a valid ECPay e-invoice notification email.",
"Please enter address first": "Please enter address first",
"Please enter configuration name": "Please enter configuration name",
"Please enter machine flavor": "Please enter machine flavor",

View File

@ -609,6 +609,7 @@
"EASY_MERCHANT_ID": "EASY_MERCHANT_ID (悠遊付)",
"ECPay Invoice": "ECPay 電子領収書",
"ECPay Invoice Settings Description": "ECPay 電子領収書設定",
"ECPay e-invoice is optional, but once any field is filled, Store ID / Hash Key / Hash IV / Email are all required.": "ECPay 電子インボイスは任意ですが、いずれかの項目を入力した場合は、店舗 ID / Hash Key / Hash IV / Email がすべて必須になります。",
"ECPay has no such invoice; marked as pending re-issue": "ECPay has no such invoice; marked as pending re-issue",
"ECPay query failed": "ECPay query failed",
"ESUN": "玉山銀行",
@ -1404,6 +1405,7 @@
"Optional notes...": "任意のメモ...",
"Optional remarks": "任意の備考",
"Optional remarks...": "任意の備考...",
"Optional. If you fill in any field, all fields below become required.": "任意。いずれかの項目を入力すると、以下のすべての項目が必須になります。",
"Or Choose Custom Date": "または日付を選択",
"Order Details": "注文詳細",
"Order Info": "注文情報",
@ -1536,6 +1538,7 @@
"Please check the form for errors.": "フォームのエラーを確認してください。",
"Please confirm the details below": "以下の詳細を確認してください",
"Please enter Webhook URL first.": "最初にWebhook URLを入力してください。",
"Please enter a valid ECPay e-invoice notification email.": "有効な ECPay 電子インボイス通知用メールアドレスを入力してください。",
"Please enter address first": "先に住所を入力してください",
"Please enter configuration name": "設定名を入力してください",
"Please enter machine flavor": "機台フレーバーを入力してください",

View File

@ -609,6 +609,7 @@
"EASY_MERCHANT_ID": "EASY_MERCHANT_ID (悠遊付)",
"ECPay Invoice": "綠界電子發票",
"ECPay Invoice Settings Description": "綠界科技電子發票設定",
"ECPay e-invoice is optional, but once any field is filled, Store ID / Hash Key / Hash IV / Email are all required.": "綠界電子發票為選填,但只要填寫任一欄位,特店 ID / Hash Key / Hash IV / Email 即全部必填。",
"ECPay has no such invoice; marked as pending re-issue": "綠界查無此發票,已標記為待補開",
"ECPay query failed": "綠界查詢失敗",
"ESUN": "玉山銀行",
@ -1404,6 +1405,7 @@
"Optional notes...": "備註內容(選填)...",
"Optional remarks": "選填備註",
"Optional remarks...": "選填備註...",
"Optional. If you fill in any field, all fields below become required.": "選填。只要填寫任一欄位,以下欄位即全部必填。",
"Or Choose Custom Date": "或選擇自訂日期",
"Order Details": "訂單詳情",
"Order Info": "單據資訊",
@ -1536,6 +1538,7 @@
"Please check the form for errors.": "請檢查欄位內容是否正確。",
"Please confirm the details below": "請確認以下詳細資訊",
"Please enter Webhook URL first.": "請先輸入 Webhook 網址。",
"Please enter a valid ECPay e-invoice notification email.": "請輸入有效的綠界電子發票通知 Email。",
"Please enter address first": "請先輸入地址",
"Please enter configuration name": "請輸入設定名稱",
"Please enter machine flavor": "請輸入機台風味",

View File

@ -98,22 +98,27 @@
</div>
</div>
<div class="space-y-6">
<div class="space-y-6" x-data="{ vals: { store_id: '', hash_key: '', hash_iv: '', email: '' }, get anyFilled() { return Object.values(this.vals).some(v => (v || '').trim() !== '') } }">
<p class="text-[11px] font-bold text-slate-400 leading-relaxed -mt-2">{{ __('Optional. If you fill in any field, all fields below become required.') }}</p>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Store ID') }}</label>
<input type="text" name="settings[ecpay_invoice][store_id]" class="luxury-input w-full">
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Store ID') }}<span x-show="anyFilled" class="text-rose-500 ml-0.5">*</span></label>
<input type="text" name="settings[ecpay_invoice][store_id]" x-model="vals.store_id" class="luxury-input w-full">
@error('settings.ecpay_invoice.store_id')<p class="mt-1.5 text-[11px] font-bold text-rose-500">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice HashKey') }}</label>
<input type="text" name="settings[ecpay_invoice][hash_key]" class="luxury-input w-full">
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice HashKey') }}<span x-show="anyFilled" class="text-rose-500 ml-0.5">*</span></label>
<input type="text" name="settings[ecpay_invoice][hash_key]" x-model="vals.hash_key" class="luxury-input w-full">
@error('settings.ecpay_invoice.hash_key')<p class="mt-1.5 text-[11px] font-bold text-rose-500">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice HashIV') }}</label>
<input type="text" name="settings[ecpay_invoice][hash_iv]" class="luxury-input w-full">
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice HashIV') }}<span x-show="anyFilled" class="text-rose-500 ml-0.5">*</span></label>
<input type="text" name="settings[ecpay_invoice][hash_iv]" x-model="vals.hash_iv" class="luxury-input w-full">
@error('settings.ecpay_invoice.hash_iv')<p class="mt-1.5 text-[11px] font-bold text-rose-500">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Notification Email') }}</label>
<input type="text" name="settings[ecpay_invoice][email]" class="luxury-input w-full">
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Notification Email') }}<span x-show="anyFilled" class="text-rose-500 ml-0.5">*</span></label>
<input type="text" name="settings[ecpay_invoice][email]" x-model="vals.email" class="luxury-input w-full">
@error('settings.ecpay_invoice.email')<p class="mt-1.5 text-[11px] font-bold text-rose-500">{{ $message }}</p>@enderror
</div>
</div>
</div>

View File

@ -102,22 +102,27 @@
</div>
</div>
<div class="space-y-6">
<div class="space-y-6" x-data="{ vals: { store_id: @js($settings['ecpay_invoice']['store_id'] ?? ''), hash_key: @js($settings['ecpay_invoice']['hash_key'] ?? ''), hash_iv: @js($settings['ecpay_invoice']['hash_iv'] ?? ''), email: @js($settings['ecpay_invoice']['email'] ?? '') }, get anyFilled() { return Object.values(this.vals).some(v => (v || '').trim() !== '') } }">
<p class="text-[11px] font-bold text-slate-400 leading-relaxed -mt-2">{{ __('Optional. If you fill in any field, all fields below become required.') }}</p>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Store ID') }}</label>
<input type="text" name="settings[ecpay_invoice][store_id]" value="{{ $settings['ecpay_invoice']['store_id'] ?? '' }}" class="luxury-input w-full">
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Store ID') }}<span x-show="anyFilled" class="text-rose-500 ml-0.5">*</span></label>
<input type="text" name="settings[ecpay_invoice][store_id]" x-model="vals.store_id" value="{{ $settings['ecpay_invoice']['store_id'] ?? '' }}" class="luxury-input w-full">
@error('settings.ecpay_invoice.store_id')<p class="mt-1.5 text-[11px] font-bold text-rose-500">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice HashKey') }}</label>
<input type="text" name="settings[ecpay_invoice][hash_key]" value="{{ $settings['ecpay_invoice']['hash_key'] ?? '' }}" class="luxury-input w-full">
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice HashKey') }}<span x-show="anyFilled" class="text-rose-500 ml-0.5">*</span></label>
<input type="text" name="settings[ecpay_invoice][hash_key]" x-model="vals.hash_key" value="{{ $settings['ecpay_invoice']['hash_key'] ?? '' }}" class="luxury-input w-full">
@error('settings.ecpay_invoice.hash_key')<p class="mt-1.5 text-[11px] font-bold text-rose-500">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice HashIV') }}</label>
<input type="text" name="settings[ecpay_invoice][hash_iv]" value="{{ $settings['ecpay_invoice']['hash_iv'] ?? '' }}" class="luxury-input w-full">
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice HashIV') }}<span x-show="anyFilled" class="text-rose-500 ml-0.5">*</span></label>
<input type="text" name="settings[ecpay_invoice][hash_iv]" x-model="vals.hash_iv" value="{{ $settings['ecpay_invoice']['hash_iv'] ?? '' }}" class="luxury-input w-full">
@error('settings.ecpay_invoice.hash_iv')<p class="mt-1.5 text-[11px] font-bold text-rose-500">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Notification Email') }}</label>
<input type="text" name="settings[ecpay_invoice][email]" value="{{ $settings['ecpay_invoice']['email'] ?? '' }}" class="luxury-input w-full">
<label class="block text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] mb-2">{{ __('Invoice Notification Email') }}<span x-show="anyFilled" class="text-rose-500 ml-0.5">*</span></label>
<input type="text" name="settings[ecpay_invoice][email]" x-model="vals.email" value="{{ $settings['ecpay_invoice']['email'] ?? '' }}" class="luxury-input w-full">
@error('settings.ecpay_invoice.email')<p class="mt-1.5 text-[11px] font-bold text-rose-500">{{ $message }}</p>@enderror
</div>
</div>
</div>