update email template
This commit is contained in:
@@ -289,7 +289,7 @@ class CustomerController extends Controller
|
||||
$customer = $this->resolveCustomer($uuid);
|
||||
|
||||
$domains = DnsDomain::query()
|
||||
->with('server:id,name,ip_address')
|
||||
->with(['server:id,name,ip_address', 'registrar:id,name,short_name,website'])
|
||||
->withCount(['ns', 'record'])
|
||||
->whereHas('customer', fn($q) => $q->where('customers.id', $customer->id))
|
||||
->orderByDesc('id')
|
||||
@@ -306,6 +306,12 @@ class CustomerController extends Controller
|
||||
'name' => $d->server->name,
|
||||
'ip_address' => $d->server->ip_address,
|
||||
] : null,
|
||||
'registrar' => $d->registrar ? [
|
||||
'id' => $d->registrar->id,
|
||||
'name' => $d->registrar->name,
|
||||
'short_name' => $d->registrar->short_name,
|
||||
'website' => $d->registrar->website,
|
||||
] : null,
|
||||
'created_at' => $d->created_at,
|
||||
'updated_at' => $d->updated_at,
|
||||
]);
|
||||
|
||||
@@ -41,7 +41,7 @@ class DomainController extends Controller
|
||||
$customerName = trim((string) $request->get('customer_name', ''));
|
||||
|
||||
$query = $this->domainRepository->model
|
||||
->with(['server', 'customer'])
|
||||
->with(['server', 'customer', 'registrar'])
|
||||
->newQuery();
|
||||
|
||||
if ($filter) {
|
||||
@@ -77,7 +77,7 @@ class DomainController extends Controller
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$domain = $this->domainRepository->model
|
||||
->with(['server', 'customer'])
|
||||
->with(['server', 'customer', 'registrar'])
|
||||
->find($id);
|
||||
|
||||
if (!$domain) {
|
||||
@@ -98,6 +98,7 @@ class DomainController extends Controller
|
||||
'customer_ids' => 'nullable|array',
|
||||
'customer_ids.*' => 'integer|exists:customers,id',
|
||||
'is_created' => 'nullable|integer|in:0,1,2',
|
||||
'registrar_id' => 'nullable|integer|exists:registrars,id',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
@@ -106,6 +107,7 @@ class DomainController extends Controller
|
||||
'domain_name' => $request->domain_name,
|
||||
'dns_manager_server_id' => $request->dns_manager_server_id,
|
||||
'is_created' => $request->is_created ?? 0,
|
||||
'registrar_id' => $request->registrar_id,
|
||||
]);
|
||||
|
||||
if (!empty($request->customer_ids)) {
|
||||
@@ -138,6 +140,7 @@ class DomainController extends Controller
|
||||
'customer_ids' => 'nullable|array',
|
||||
'customer_ids.*' => 'integer|exists:customers,id',
|
||||
'is_created' => 'nullable|integer|in:0,1,2',
|
||||
'registrar_id' => 'nullable|integer|exists:registrars,id',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
@@ -146,6 +149,7 @@ class DomainController extends Controller
|
||||
'domain_name',
|
||||
'dns_manager_server_id',
|
||||
'is_created',
|
||||
'registrar_id',
|
||||
]));
|
||||
|
||||
// Sync customers (empty array = detach all)
|
||||
@@ -202,6 +206,12 @@ class DomainController extends Controller
|
||||
'email' => $c->email,
|
||||
])->values()->toArray(),
|
||||
'is_created' => $domain->is_created,
|
||||
'registrar_id' => $domain->registrar_id,
|
||||
'registrar' => $domain->registrar ? [
|
||||
'id' => $domain->registrar->id,
|
||||
'name' => $domain->registrar->name,
|
||||
'short_name' => $domain->registrar->short_name,
|
||||
] : null,
|
||||
'created_at' => $domain->created_at,
|
||||
'updated_at' => $domain->updated_at,
|
||||
];
|
||||
|
||||
@@ -9,20 +9,17 @@ use App\Http\Requests\Api\Admin\Domain\ConfirmRegistrationRequest;
|
||||
use App\Http\Requests\Api\Admin\Domain\RejectAndRefundRequest;
|
||||
use App\Http\Requests\Api\Admin\Domain\UpdateKycUrlRequest;
|
||||
use App\Http\Requests\Api\Admin\Domain\UpdateRegistrantInfoRequest;
|
||||
use App\Mail\Customer\DomainOrderStatusMail;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Order;
|
||||
use App\Models\Service;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Template;
|
||||
use App\Repositories\Api\Domain\DomainOrderRepository;
|
||||
use App\Services\Api\Domain\DomainOrderService;
|
||||
use App\Services\Api\Domain\DomainRegistrantService;
|
||||
use App\Services\Email\EmailTemplateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class AdminDomainOrderController extends Controller
|
||||
{
|
||||
@@ -265,9 +262,9 @@ class AdminDomainOrderController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
[$title, $body, $templateName] = $status === 'approved'
|
||||
? ["Tên miền {$domainName} đã được duyệt", "Tên miền {$domainName} (#{$orderNum}) đã được kích hoạt thành công.", 'Domain Order Approved']
|
||||
: ["Tên miền {$domainName} bị từ chối", "Tên miền {$domainName} (#{$orderNum}) đã bị từ chối. Lý do: {$reason}. Số dư đã được hoàn lại.", 'Domain Order Rejected'];
|
||||
[$title, $body, $scenario] = $status === 'approved'
|
||||
? ["Tên miền {$domainName} đã được duyệt", "Tên miền {$domainName} (#{$orderNum}) đã được kích hoạt thành công.", 'domain_order_approved']
|
||||
: ["Tên miền {$domainName} bị từ chối", "Tên miền {$domainName} (#{$orderNum}) đã bị từ chối. Lý do: {$reason}. Số dư đã được hoàn lại.", 'domain_order_rejected'];
|
||||
|
||||
Notification::notifyCustomer($customer, [
|
||||
'type' => 'domain_order_' . $status,
|
||||
@@ -277,22 +274,21 @@ class AdminDomainOrderController extends Controller
|
||||
]);
|
||||
|
||||
try {
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
|
||||
|
||||
$template = Template::where('name', $templateName)->first();
|
||||
if (!$template) {
|
||||
if (!\App\Models\Template::where('scenario', $scenario)->where('status', 1)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mail::to($customer->email)->send(new DomainOrderStatusMail($template, [
|
||||
$setting = \App\Models\Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
|
||||
|
||||
app(EmailTemplateService::class)->send($scenario, $customer->email, [
|
||||
'company_name' => $settingValue['company'] ?? config('app.name'),
|
||||
'company_logo' => $settingValue['logo'] ?? '',
|
||||
'domain_name' => $domainName,
|
||||
'client_name' => $customer->fullname,
|
||||
'order_num' => $orderNum,
|
||||
'reason' => $reason,
|
||||
]));
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('AdminDomainOrderController: không thể gửi email khách hàng: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\EmailTemplate;
|
||||
|
||||
use App\Exceptions\InvalidTemplateVariableException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\GenericTemplateMail;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Template;
|
||||
use App\Services\Email\EmailTemplateRenderer;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class EmailTemplateController extends Controller
|
||||
{
|
||||
@@ -32,16 +37,39 @@ class EmailTemplateController extends Controller
|
||||
return response()->json(['error' => false, 'data' => $this->format($template)]);
|
||||
}
|
||||
|
||||
/** Danh sách scenario hợp lệ (key + label) để render dropdown động ở frontend. */
|
||||
public function scenarios(): JsonResponse
|
||||
{
|
||||
$used = Template::whereNotNull('scenario')->pluck('scenario')->all();
|
||||
|
||||
$data = collect(Template::SCENARIOS)->map(fn ($label, $key) => [
|
||||
'key' => $key,
|
||||
'label' => $label,
|
||||
'has_template' => in_array($key, $used, true),
|
||||
])->values();
|
||||
|
||||
return response()->json(['error' => false, 'data' => $data]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => 'nullable|string|max:255',
|
||||
'scenario' => 'nullable|string|in:vm_credentials,renewal_reminder,news_notification,invoice_pdf',
|
||||
'scenario' => ['nullable', 'string', Rule::in(array_keys(Template::SCENARIOS))],
|
||||
'format' => 'nullable|in:html,json',
|
||||
'invoice_template_id' => 'nullable|integer|exists:template,id',
|
||||
'subject' => 'nullable|string|max:255',
|
||||
'message' => 'nullable|string',
|
||||
'status' => 'boolean',
|
||||
'mail_config' => 'nullable|array',
|
||||
'mail_config.from_email' => 'nullable|email',
|
||||
'mail_config.from_name' => 'nullable|string|max:255',
|
||||
'mail_config.reply_to' => 'nullable|email',
|
||||
'mail_config.reply_to_name' => 'nullable|string|max:255',
|
||||
'mail_config.cc' => 'nullable|array',
|
||||
'mail_config.cc.*' => 'email',
|
||||
'mail_config.bcc' => 'nullable|array',
|
||||
'mail_config.bcc.*' => 'email',
|
||||
]);
|
||||
|
||||
$template = DB::transaction(fn() => Template::create([
|
||||
@@ -53,6 +81,7 @@ class EmailTemplateController extends Controller
|
||||
'subject' => $data['subject'] ?? null,
|
||||
'message' => $data['message'] ?? null,
|
||||
'status' => $data['status'] ?? 1,
|
||||
'metadata' => isset($data['mail_config']) ? ['mail' => $data['mail_config']] : null,
|
||||
]));
|
||||
|
||||
return response()->json([
|
||||
@@ -67,14 +96,31 @@ class EmailTemplateController extends Controller
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => 'sometimes|nullable|string|max:255',
|
||||
'scenario' => 'sometimes|nullable|string|in:vm_credentials,renewal_reminder,news_notification,invoice_pdf',
|
||||
'scenario' => ['sometimes', 'nullable', 'string', Rule::in(array_keys(Template::SCENARIOS))],
|
||||
'format' => 'sometimes|nullable|in:html,json',
|
||||
'invoice_template_id' => 'sometimes|nullable|integer|exists:template,id',
|
||||
'subject' => 'sometimes|nullable|string|max:255',
|
||||
'message' => 'sometimes|nullable|string',
|
||||
'status' => 'sometimes|boolean',
|
||||
'mail_config' => 'sometimes|nullable|array',
|
||||
'mail_config.from_email' => 'nullable|email',
|
||||
'mail_config.from_name' => 'nullable|string|max:255',
|
||||
'mail_config.reply_to' => 'nullable|email',
|
||||
'mail_config.reply_to_name' => 'nullable|string|max:255',
|
||||
'mail_config.cc' => 'nullable|array',
|
||||
'mail_config.cc.*' => 'email',
|
||||
'mail_config.bcc' => 'nullable|array',
|
||||
'mail_config.bcc.*' => 'email',
|
||||
]);
|
||||
|
||||
// metadata.variables do developer khai báo theo code — admin chỉ được sửa metadata.mail.
|
||||
if (array_key_exists('mail_config', $data)) {
|
||||
$metadata = $template->metadata ?? [];
|
||||
$metadata['mail'] = $data['mail_config'] ?? [];
|
||||
$data['metadata'] = $metadata;
|
||||
}
|
||||
unset($data['mail_config']);
|
||||
|
||||
DB::transaction(fn() => $template->update($data));
|
||||
|
||||
return response()->json([
|
||||
@@ -122,6 +168,106 @@ class EmailTemplateController extends Controller
|
||||
return response()->json(['error' => false, 'message' => 'Đã lưu cài đặt email.', 'data' => json_decode($setting->value, true)]);
|
||||
}
|
||||
|
||||
// ── Preview (render thật qua EmailTemplateRenderer, dùng example value) ────
|
||||
|
||||
public function preview(Request $request, int $id, EmailTemplateRenderer $renderer): JsonResponse
|
||||
{
|
||||
$template = Template::findOrFail($id);
|
||||
|
||||
if ($template->format === 'json') {
|
||||
return response()->json(['error' => true, 'message' => 'Template hoá đơn JSON dùng preview riêng (previewInvoiceJson).'], 422);
|
||||
}
|
||||
|
||||
$overrides = $request->validate([
|
||||
'subject' => 'nullable|string|max:255',
|
||||
'message' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// Cho phép preview nội dung đang sửa dở (chưa lưu) bằng cách override tạm, không đụng DB.
|
||||
$draft = clone $template;
|
||||
if (array_key_exists('subject', $overrides)) {
|
||||
$draft->subject = $overrides['subject'];
|
||||
}
|
||||
if (array_key_exists('message', $overrides)) {
|
||||
$draft->message = $overrides['message'];
|
||||
}
|
||||
|
||||
$exampleData = collect($template->variables())
|
||||
->mapWithKeys(fn ($def, $key) => [$key => $def['example'] ?? "[{$key}]"])
|
||||
->all();
|
||||
|
||||
try {
|
||||
$rendered = $renderer->render($draft, $exampleData);
|
||||
} catch (InvalidTemplateVariableException $e) {
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 422);
|
||||
}
|
||||
|
||||
return response()->json(['error' => false, 'data' => $rendered]);
|
||||
}
|
||||
|
||||
// ── Send test email ───────────────────────────────────────────────────────
|
||||
|
||||
public function sendTest(Request $request, int $id, EmailTemplateRenderer $renderer): JsonResponse
|
||||
{
|
||||
$template = Template::findOrFail($id);
|
||||
|
||||
if ($template->format === 'json') {
|
||||
return response()->json(['error' => true, 'message' => 'Không thể gửi mẫu cho template JSON (hoá đơn).'], 422);
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'to' => 'required|string',
|
||||
'cc' => 'nullable|string',
|
||||
'bcc' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$to = $this->parseRecipients($data['to']);
|
||||
if (empty($to)) {
|
||||
return response()->json(['error' => true, 'message' => 'Vui lòng nhập email người nhận hợp lệ.'], 422);
|
||||
}
|
||||
|
||||
$exampleData = collect($template->variables())
|
||||
->mapWithKeys(fn ($def, $key) => [$key => $def['example'] ?? "[{$key}]"])
|
||||
->all();
|
||||
|
||||
try {
|
||||
$rendered = $renderer->render($template, $exampleData);
|
||||
|
||||
$mailer = Mail::to($to);
|
||||
if ($cc = $this->parseRecipients($data['cc'] ?? null)) {
|
||||
$mailer->cc($cc);
|
||||
}
|
||||
if ($bcc = $this->parseRecipients($data['bcc'] ?? null)) {
|
||||
$mailer->bcc($bcc);
|
||||
}
|
||||
$mailer->send(new GenericTemplateMail(
|
||||
'[TEST] ' . $rendered['subject'],
|
||||
$rendered['html'],
|
||||
$template->mailConfig(),
|
||||
));
|
||||
} catch (InvalidTemplateVariableException $e) {
|
||||
return response()->json(['error' => true, 'message' => $e->getMessage()], 422);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['error' => true, 'message' => 'Không thể gửi email: ' . $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã gửi email mẫu.']);
|
||||
}
|
||||
|
||||
private function parseRecipients(?string $value): array
|
||||
{
|
||||
if (!$value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect(preg_split('/[,;]+/', $value) ?: [])
|
||||
->map(fn ($email) => trim((string) $email))
|
||||
->filter(fn ($email) => $email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
// ── Invoice PDF preview ───────────────────────────────────────────────────
|
||||
|
||||
public function previewInvoiceJson(int $id): JsonResponse
|
||||
@@ -154,6 +300,8 @@ class EmailTemplateController extends Controller
|
||||
'subject' => $t->subject,
|
||||
'message' => $t->message,
|
||||
'status' => $t->status,
|
||||
'variables' => array_values($t->variables()),
|
||||
'mail_config' => $t->mailConfig(),
|
||||
'created_at' => $t->created_at,
|
||||
'updated_at' => $t->updated_at,
|
||||
];
|
||||
|
||||
@@ -35,8 +35,7 @@ class EmployeeController extends Controller
|
||||
DB::beginTransaction();
|
||||
|
||||
$employee = $this->employeeRepository->create([
|
||||
'firstname' => $request->firstname,
|
||||
'lastname' => $request->lastname,
|
||||
'fullname' => $request->fullname,
|
||||
'username' => $request->username,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone,
|
||||
@@ -97,8 +96,7 @@ class EmployeeController extends Controller
|
||||
DB::beginTransaction();
|
||||
|
||||
$data = [
|
||||
'firstname' => $request->firstname,
|
||||
'lastname' => $request->lastname,
|
||||
'fullname' => $request->fullname,
|
||||
'username' => $request->username,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone,
|
||||
|
||||
@@ -3,16 +3,19 @@
|
||||
namespace App\Http\Controllers\Api\Admin\Invoice;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Helpers\OrderHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\InvoiceRequest;
|
||||
use App\Http\Resources\Api\Admin\InvoiceResource;
|
||||
use App\Mail\InvoiceNotificationMail;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
use App\Repositories\Invoice\InvoiceRepositoryInterface;
|
||||
use App\Repositories\InvoiceAttachment\InvoiceAttachmentRepositoryInterface;
|
||||
use App\Repositories\InvoiceItem\InvoiceItemRepositoryInterface;
|
||||
use App\Repositories\Order\OrderRepositoryInterface;
|
||||
use App\Services\InvoicePaymentService;
|
||||
use App\Services\InvoicePdfService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -28,7 +31,6 @@ class InvoiceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InvoiceRepositoryInterface $invoiceRepository,
|
||||
private readonly OrderRepositoryInterface $orderRepository,
|
||||
private readonly InvoiceItemRepositoryInterface $invoiceItemRepository,
|
||||
private readonly InvoiceAttachmentRepositoryInterface $attachmentRepository,
|
||||
private readonly InvoicePdfService $invoicePdfService,
|
||||
@@ -76,38 +78,63 @@ class InvoiceController extends Controller
|
||||
->make(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin tạo thêm 1 invoice thủ công cho 1 order đã tồn tại — chỉ dùng cho phụ
|
||||
* phí/điều chỉnh thuộc CHÍNH dịch vụ trong order đó (vd. phạt trễ hạn, phí xử lý).
|
||||
* Order cố định: không dùng endpoint này để bán thêm dịch vụ mới — dịch vụ mới phải
|
||||
* là 1 Order riêng (qua OrderController::store). Đi qua cùng convention với renewal
|
||||
* invoice ở nơi khác trong codebase: gắn order_id vào order gốc, tự sinh invoice_number.
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'customer_id' => 'required|numeric',
|
||||
'payment_method' => 'required|numeric',
|
||||
'sub_total' => 'required|numeric',
|
||||
'total' => 'required|numeric',
|
||||
'order_id' => 'required|integer|exists:orders,id',
|
||||
'invoice_type' => 'nullable|integer|in:1,2,3',
|
||||
'description' => 'nullable|string|max:500',
|
||||
'due_date' => 'nullable|date',
|
||||
'sub_total' => 'required|numeric|min:0',
|
||||
'tax' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
$invoiceData = [
|
||||
'date' => time(),
|
||||
'due_date' => time() + (7 * 24 * 60 * 60),
|
||||
'sub_total' => $validated['sub_total'],
|
||||
'tax' => $validated['sub_total'] * 0.1,
|
||||
'total' => $validated['total'],
|
||||
'payment_method' => $validated['payment_method'],
|
||||
'customer_id' => $validated['customer_id'],
|
||||
'status' => 0,
|
||||
];
|
||||
$order = Order::find($validated['order_id']);
|
||||
$subTotal = (int) $validated['sub_total'];
|
||||
$tax = (int) ($validated['tax'] ?? 0);
|
||||
$total = $subTotal + $tax;
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$invoice = $this->invoiceRepository->create($invoiceData);
|
||||
DB::commit();
|
||||
$invoice = DB::transaction(function () use ($order, $validated, $subTotal, $tax, $total) {
|
||||
$invoice = Invoice::create([
|
||||
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
|
||||
'customer_id' => $order->customer_id,
|
||||
'order_id' => $order->id,
|
||||
'invoice_type' => $validated['invoice_type'] ?? 3,
|
||||
'sub_total' => $subTotal,
|
||||
'discount_amount' => 0,
|
||||
'tax' => $tax,
|
||||
'total' => $total,
|
||||
'status' => InvoiceStatus::UNPAID->value,
|
||||
'due_date' => $validated['due_date'] ?? now()->addDays(7)->toDateString(),
|
||||
]);
|
||||
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $invoice->id,
|
||||
'type' => 'service',
|
||||
'description' => $validated['description'] ?: 'Hóa đơn bổ sung',
|
||||
'months' => 0,
|
||||
'quantity' => 1,
|
||||
'unit_price' => $subTotal,
|
||||
'total_price' => $subTotal,
|
||||
]);
|
||||
|
||||
return $invoice;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Invoice created successfully',
|
||||
'data' => new InvoiceResource($invoice)
|
||||
'message' => 'Đã tạo hóa đơn',
|
||||
'data' => new InvoiceResource($invoice->load('invoiceItems')),
|
||||
], 201);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage()
|
||||
@@ -199,17 +226,17 @@ class InvoiceController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$this->orderRepository->updateBy('invoice_id', $id, ['invoice_id' => 0]);
|
||||
// Note: `orders` has no `invoice_id` column (the FK runs the other way —
|
||||
// invoices.order_id -> orders.id), so there's nothing to clean up there. Any
|
||||
// `services.invoice_id` pointing at this invoice is already handled by the
|
||||
// FK's onDelete('set null').
|
||||
$this->invoiceRepository->delete($id);
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Invoice deleted successfully'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage()
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\Admin\Transaction;
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\Customer\ManualDepositStatus;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Log;
|
||||
use App\Models\Transaction;
|
||||
@@ -85,11 +86,55 @@ class AdminTransactionController extends Controller
|
||||
|
||||
public function approve(string $uuid): JsonResponse
|
||||
{
|
||||
$transaction = Transaction::with('customer')
|
||||
->where('uuid', $uuid)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->first();
|
||||
try {
|
||||
// Lock the transaction (and customer) row for the whole read-check-write so two
|
||||
// admins approving the same transaction concurrently cannot both credit the customer.
|
||||
$transaction = DB::transaction(function () use ($uuid) {
|
||||
$transaction = Transaction::where('uuid', $uuid)
|
||||
->where('type', 'manual')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$transaction || (int) $transaction->status !== TransactionStatus::PENDING->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$transaction->update(['status' => TransactionStatus::COMPLETED->value]);
|
||||
|
||||
$customer = Customer::where('id', $transaction->customer_id)->lockForUpdate()->first();
|
||||
$customer->total += $transaction->amount;
|
||||
$customer->credit += (int) floor($transaction->amount / 100);
|
||||
$customer->total_pending = max(0, $customer->total_pending - $transaction->amount);
|
||||
$customer->save();
|
||||
|
||||
Log::create([
|
||||
'agent' => 'admin',
|
||||
'client_ip' => request()->ip(),
|
||||
'description' => 'Admin duyệt nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ (+' . floor($transaction->amount / 100) . ' credit)',
|
||||
'customer_id' => $customer->id,
|
||||
]);
|
||||
|
||||
Notification::notifyCustomer($customer, [
|
||||
'type' => 'deposit_success',
|
||||
'title' => 'Nạp tiền thành công',
|
||||
'body' => 'Giao dịch ' . $transaction->public_code . ' đã được duyệt.',
|
||||
'link' => '/lich-su-giao-dich/' . $transaction->id,
|
||||
'data' => [
|
||||
'transaction_uuid' => $transaction->uuid,
|
||||
'amount' => $transaction->amount,
|
||||
'public_code' => $transaction->public_code,
|
||||
],
|
||||
]);
|
||||
|
||||
return $transaction->setRelation('customer', $customer);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
FacadesLog::error('AdminTransactionController::approve error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Có lỗi xảy ra, vui lòng thử lại',
|
||||
], 500);
|
||||
}
|
||||
|
||||
if (!$transaction) {
|
||||
return response()->json([
|
||||
@@ -98,65 +143,28 @@ class AdminTransactionController extends Controller
|
||||
], 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
$customer = $transaction->customer;
|
||||
|
||||
try {
|
||||
$transaction->update(['status' => TransactionStatus::COMPLETED->value]);
|
||||
|
||||
$customer = $transaction->customer;
|
||||
$customer->total += $transaction->amount;
|
||||
$customer->credit += (int) floor($transaction->amount / 100);
|
||||
$customer->total_pending = max(0, $customer->total_pending - $transaction->amount);
|
||||
$customer->save();
|
||||
|
||||
Log::create([
|
||||
'agent' => 'admin',
|
||||
'client_ip' => request()->ip(),
|
||||
'description' => 'Admin duyệt nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ (+' . floor($transaction->amount / 100) . ' credit)',
|
||||
Mail::to($customer->email)->send(new ManualDepositStatus([
|
||||
'customer_id' => $customer->id,
|
||||
'customer_email' => $customer->email,
|
||||
'customer_name' => $customer->fullname,
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'amount' => $transaction->amount,
|
||||
'status_label' => 'Đã duyệt',
|
||||
'status_color' => '#15803d',
|
||||
'status_bg' => '#f0fdf4',
|
||||
'subject' => 'Yêu cầu nạp tiền đã được duyệt - ' . $transaction->public_code,
|
||||
'status_message' => 'Yêu cầu nạp tiền của bạn đã được duyệt. Số tiền đã được cộng vào tài khoản và có thể sử dụng ngay.',
|
||||
'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'),
|
||||
'processed_date' => now()->format('d/m/Y H:i:s'),
|
||||
]));
|
||||
} catch (\Throwable $e) {
|
||||
FacadesLog::error('Không thể gửi email duyệt nạp tiền thủ công: ' . $e->getMessage(), [
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'customer_email' => $customer->email,
|
||||
]);
|
||||
|
||||
Notification::notifyCustomer($customer, [
|
||||
'type' => 'deposit_success',
|
||||
'title' => 'Nạp tiền thành công',
|
||||
'body' => 'Giao dịch ' . $transaction->public_code . ' đã được duyệt.',
|
||||
'link' => '/lich-su-giao-dich/' . $transaction->id,
|
||||
'data' => [
|
||||
'transaction_uuid' => $transaction->uuid,
|
||||
'amount' => $transaction->amount,
|
||||
'public_code' => $transaction->public_code,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
try {
|
||||
Mail::to($customer->email)->send(new ManualDepositStatus([
|
||||
'customer_id' => $customer->id,
|
||||
'customer_email' => $customer->email,
|
||||
'customer_name' => $customer->fullname,
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'amount' => $transaction->amount,
|
||||
'status_label' => 'Đã duyệt',
|
||||
'status_color' => '#15803d',
|
||||
'status_bg' => '#f0fdf4',
|
||||
'subject' => 'Yêu cầu nạp tiền đã được duyệt - ' . $transaction->public_code,
|
||||
'status_message' => 'Yêu cầu nạp tiền của bạn đã được duyệt. Số tiền đã được cộng vào tài khoản và có thể sử dụng ngay.',
|
||||
'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'),
|
||||
'processed_date' => now()->format('d/m/Y H:i:s'),
|
||||
]));
|
||||
} catch (\Throwable $e) {
|
||||
FacadesLog::error('Không thể gửi email duyệt nạp tiền thủ công: ' . $e->getMessage(), [
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'customer_email' => $customer->email,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
FacadesLog::error('AdminTransactionController::approve error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Có lỗi xảy ra, vui lòng thử lại',
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
@@ -167,11 +175,41 @@ class AdminTransactionController extends Controller
|
||||
|
||||
public function reject(string $uuid): JsonResponse
|
||||
{
|
||||
$transaction = Transaction::with('customer')
|
||||
->where('uuid', $uuid)
|
||||
->where('type', 'manual')
|
||||
->where('status', TransactionStatus::PENDING->value)
|
||||
->first();
|
||||
try {
|
||||
// Same lock-then-verify-status pattern as approve() — prevents a duplicate
|
||||
// reject (or reject-after-approve) race from double-adjusting total_pending.
|
||||
$transaction = DB::transaction(function () use ($uuid) {
|
||||
$transaction = Transaction::where('uuid', $uuid)
|
||||
->where('type', 'manual')
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$transaction || (int) $transaction->status !== TransactionStatus::PENDING->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$transaction->update(['status' => TransactionStatus::FAILED->value]);
|
||||
|
||||
$customer = Customer::where('id', $transaction->customer_id)->lockForUpdate()->first();
|
||||
$customer->total_pending = max(0, $customer->total_pending - $transaction->amount);
|
||||
$customer->save();
|
||||
|
||||
Log::create([
|
||||
'agent' => 'admin',
|
||||
'client_ip' => request()->ip(),
|
||||
'description' => 'Admin từ chối nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ',
|
||||
'customer_id' => $customer->id,
|
||||
]);
|
||||
|
||||
return $transaction->setRelation('customer', $customer);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
FacadesLog::error('AdminTransactionController::reject error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Có lỗi xảy ra, vui lòng thử lại',
|
||||
], 500);
|
||||
}
|
||||
|
||||
if (!$transaction) {
|
||||
return response()->json([
|
||||
@@ -180,51 +218,28 @@ class AdminTransactionController extends Controller
|
||||
], 404);
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
$customer = $transaction->customer;
|
||||
|
||||
try {
|
||||
$transaction->update(['status' => TransactionStatus::FAILED->value]);
|
||||
|
||||
$customer = $transaction->customer;
|
||||
$customer->total_pending = max(0, $customer->total_pending - $transaction->amount);
|
||||
$customer->save();
|
||||
|
||||
Log::create([
|
||||
'agent' => 'admin',
|
||||
'client_ip' => request()->ip(),
|
||||
'description' => 'Admin từ chối nạp tiền thủ công: ' . number_format($transaction->amount) . ' VNĐ',
|
||||
Mail::to($customer->email)->send(new ManualDepositStatus([
|
||||
'customer_id' => $customer->id,
|
||||
'customer_email' => $customer->email,
|
||||
'customer_name' => $customer->fullname,
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'amount' => $transaction->amount,
|
||||
'status_label' => 'Không phù hợp',
|
||||
'status_color' => '#b91c1c',
|
||||
'status_bg' => '#fef2f2',
|
||||
'subject' => 'Yêu cầu nạp tiền không phù hợp - ' . $transaction->public_code,
|
||||
'status_message' => 'Yêu cầu nạp tiền của bạn chưa phù hợp để được duyệt. Vui lòng kiểm tra lại chứng từ hoặc liên hệ bộ phận hỗ trợ để được hướng dẫn thêm.',
|
||||
'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'),
|
||||
'processed_date' => now()->format('d/m/Y H:i:s'),
|
||||
]));
|
||||
} catch (\Throwable $e) {
|
||||
FacadesLog::error('Không thể gửi email từ chối nạp tiền thủ công: ' . $e->getMessage(), [
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'customer_email' => $customer->email,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
try {
|
||||
Mail::to($customer->email)->send(new ManualDepositStatus([
|
||||
'customer_id' => $customer->id,
|
||||
'customer_email' => $customer->email,
|
||||
'customer_name' => $customer->fullname,
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'amount' => $transaction->amount,
|
||||
'status_label' => 'Không phù hợp',
|
||||
'status_color' => '#b91c1c',
|
||||
'status_bg' => '#fef2f2',
|
||||
'subject' => 'Yêu cầu nạp tiền không phù hợp - ' . $transaction->public_code,
|
||||
'status_message' => 'Yêu cầu nạp tiền của bạn chưa phù hợp để được duyệt. Vui lòng kiểm tra lại chứng từ hoặc liên hệ bộ phận hỗ trợ để được hướng dẫn thêm.',
|
||||
'deposit_date' => $transaction->created_at?->format('d/m/Y H:i:s') ?? now()->format('d/m/Y H:i:s'),
|
||||
'processed_date' => now()->format('d/m/Y H:i:s'),
|
||||
]));
|
||||
} catch (\Throwable $e) {
|
||||
FacadesLog::error('Không thể gửi email từ chối nạp tiền thủ công: ' . $e->getMessage(), [
|
||||
'transaction_code' => $transaction->public_code,
|
||||
'customer_email' => $customer->email,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
FacadesLog::error('AdminTransactionController::reject error: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Có lỗi xảy ra, vui lòng thử lại',
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
|
||||
Reference in New Issue
Block a user