feat(billing): renewals, coupons, VAT numbering, billing cycles and invoice payment tracking
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
ea9a53f742
commit
25f23d541e
@@ -21,7 +21,8 @@ trait ManagesCustomerBilling
|
||||
'date' => 'nullable|date',
|
||||
'due_date' => 'nullable|date',
|
||||
'date_paid' => 'nullable|date',
|
||||
'note' => 'nullable|string',
|
||||
'note' => 'nullable|string|max:2000',
|
||||
'amount_paid' => 'nullable|integer|min:0',
|
||||
'sub_total' => 'nullable|integer|min:0',
|
||||
'tax' => 'nullable|integer|min:0',
|
||||
'total' => 'nullable|integer|min:0',
|
||||
|
||||
@@ -2,10 +2,20 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Customer\Concerns;
|
||||
|
||||
use App\Models\ActivityLog;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceAttachment;
|
||||
use App\Models\Service;
|
||||
use App\Support\AutoEmailGate;
|
||||
use App\Services\Api\Order\OrderQueryService;
|
||||
use App\Services\ServiceRenewalNotifier;
|
||||
use App\Services\ServiceRenewalReminderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Support\BillingCycleCatalog;
|
||||
use App\Support\RenewalPeriodGuard;
|
||||
|
||||
trait ManagesCustomerServices
|
||||
{
|
||||
@@ -44,7 +54,7 @@ trait ManagesCustomerServices
|
||||
'requirements' => 'nullable|string',
|
||||
'feedback' => 'nullable|string',
|
||||
'price' => 'nullable|numeric|min:0',
|
||||
'billing_cycle' => 'nullable|in:monthly,quarterly,semi_annually,annually,biennially,triennially',
|
||||
'billing_cycle' => ['nullable', Rule::in(BillingCycleCatalog::codes())],
|
||||
'virtual_machine_id' => 'nullable|integer|exists:virtual_machines,id',
|
||||
'os_id' => 'nullable|integer|exists:os,id',
|
||||
'vm_cpu' => 'nullable|integer|min:1',
|
||||
@@ -84,12 +94,113 @@ trait ManagesCustomerServices
|
||||
return response()->json(['error' => false, 'message' => 'Đã đổi port thành công', 'data' => $service]);
|
||||
}
|
||||
|
||||
public function notifyServiceRenewal(string $uuid, int $serviceId): JsonResponse
|
||||
public function notifyServiceRenewal(
|
||||
string $uuid,
|
||||
int $serviceId,
|
||||
ServiceRenewalReminderService $reminders,
|
||||
): JsonResponse
|
||||
{
|
||||
AutoEmailGate::markManual();
|
||||
$this->serviceProvisioning->notifyRenewal($uuid, $serviceId);
|
||||
$customer = $this->resolveCustomer($uuid);
|
||||
$service = Service::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('id', $serviceId)
|
||||
->first();
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã gửi thông báo gia hạn đến khách hàng.']);
|
||||
abort_if(! $service, 404, 'Không tìm thấy dịch vụ');
|
||||
abort_if(! $customer->email, 422, 'Khách hàng chưa có email');
|
||||
abort_if(! $service->expires_at, 422, 'Dịch vụ chưa có ngày hết hạn');
|
||||
|
||||
$reminders->send([$service->id], Auth::guard('admin')->user());
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã đưa email nhắc gia hạn vào hàng đợi gửi và ghi nhận lịch sử.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function serviceRenewals(string $uuid, int $serviceId): JsonResponse
|
||||
{
|
||||
$service = Service::where('customer_id', $this->resolveCustomer($uuid)->id)->findOrFail($serviceId);
|
||||
|
||||
$renewals = ActivityLog::where('subject_type', Service::class)
|
||||
->where('subject_id', $service->id)
|
||||
->where('event', 'renewed')
|
||||
->latest('id')
|
||||
->limit(100)
|
||||
->get(['id', 'causer_type', 'causer_name', 'old_values', 'new_values', 'created_at']);
|
||||
|
||||
return response()->json(['error' => false, 'data' => $renewals]);
|
||||
}
|
||||
|
||||
public function renewService(string $uuid, int $serviceId, Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'months' => 'required|integer|min:1|max:120',
|
||||
'payment' => 'required|in:balance,manual',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'proof' => ['nullable', ...array_slice(explode('|', InvoiceAttachment::UPLOAD_RULE), 1)],
|
||||
]);
|
||||
|
||||
$customer = $this->resolveCustomer($uuid);
|
||||
$current = Service::where('customer_id', $customer->id)->find($serviceId);
|
||||
$previousExpiresAt = $current?->expires_at;
|
||||
|
||||
if ($current && $violation = RenewalPeriodGuard::violation($current, (int) $data['months'])) {
|
||||
return response()->json(['error' => true, 'message' => $violation], 422);
|
||||
}
|
||||
|
||||
$result = app(OrderQueryService::class)->renewServiceByAdmin(
|
||||
$serviceId,
|
||||
$customer,
|
||||
(int) $data['months'],
|
||||
$data['payment'],
|
||||
(int) auth()->id(),
|
||||
(string) ($data['note'] ?? ''),
|
||||
);
|
||||
|
||||
if (! $result['ok']) {
|
||||
return response()->json(
|
||||
['error' => true, 'message' => $result['message'], 'data' => $result['data'] ?? null],
|
||||
$result['code'],
|
||||
);
|
||||
}
|
||||
|
||||
$this->notifyRenewal($serviceId, (int) $result['data']['invoice_id'], (int) $data['months'], $previousExpiresAt, $data['payment']);
|
||||
|
||||
if ($data['payment'] === 'manual' && $request->hasFile('proof')) {
|
||||
$this->storeRenewalProof($request->file('proof'), (int) $result['data']['invoice_id']);
|
||||
}
|
||||
|
||||
return response()->json(['error' => false, 'message' => $result['message'], 'data' => $result['data']]);
|
||||
}
|
||||
|
||||
private function notifyRenewal(int $serviceId, int $invoiceId, int $months, ?\Carbon\CarbonInterface $previousExpiresAt, string $payment): void
|
||||
{
|
||||
$admin = auth()->user();
|
||||
|
||||
app(ServiceRenewalNotifier::class)->notifyAdminRenewal(
|
||||
Service::with(['customer', 'product'])->findOrFail($serviceId),
|
||||
Invoice::findOrFail($invoiceId),
|
||||
$months,
|
||||
$previousExpiresAt,
|
||||
$payment,
|
||||
(string) ($admin?->fullname ?? $admin?->username ?? 'Admin'),
|
||||
$admin?->id,
|
||||
);
|
||||
}
|
||||
|
||||
private function storeRenewalProof(UploadedFile $file, int $invoiceId): void
|
||||
{
|
||||
InvoiceAttachment::create([
|
||||
'invoice_id' => $invoiceId,
|
||||
'type' => InvoiceAttachment::TYPE_PAYMENT_PROOF,
|
||||
'filename' => $file->getClientOriginalName(),
|
||||
'path' => $file->store("invoice-attachments/{$invoiceId}", 'public'),
|
||||
'disk' => 'public',
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
'uploaded_by' => auth()->user()?->username ?? 'admin',
|
||||
]);
|
||||
}
|
||||
|
||||
public function createService(string $uuid, Request $request): JsonResponse
|
||||
@@ -103,7 +214,7 @@ trait ManagesCustomerServices
|
||||
'started_at' => 'nullable|date',
|
||||
'expires_at' => 'nullable|date',
|
||||
'note' => 'nullable|string',
|
||||
'billing_cycle' => 'nullable|in:monthly,quarterly,semi_annually,annually,biennially,triennially',
|
||||
'billing_cycle' => ['nullable', Rule::in(BillingCycleCatalog::codes())],
|
||||
'payment_method' => 'required|in:transfer,balance',
|
||||
'amount_paid' => 'nullable|numeric|min:0',
|
||||
'proof_images' => 'nullable|array',
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Customer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\BillingProfileRequest;
|
||||
use App\Models\Customer;
|
||||
use App\Support\CustomerBillingProfile;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class CustomerBillingProfileController extends Controller
|
||||
{
|
||||
public function show(string $uuid): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => CustomerBillingProfile::payload(Customer::where('uuid', $uuid)->firstOrFail()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(BillingProfileRequest $request, string $uuid): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã lưu thông tin xuất hóa đơn',
|
||||
'data' => CustomerBillingProfile::save(Customer::where('uuid', $uuid)->firstOrFail(), $request->validated()),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user