This commit is contained in:
baodev0
2026-09-23 15:17:18 +07:00
parent cd38c5d6d0
commit 82a71244ca
107 changed files with 3332 additions and 3306 deletions
+20 -1
View File
@@ -24,6 +24,16 @@ class Customer extends Authenticatable implements JWTSubject
return ['uuid'];
}
/**
* Điểm tích luỹ cộng cho một khoản nạp: 1 credit / 100 VNĐ.
* Trước đây khớp sao kê tự động cộng credit bằng nguyên số tiền còn duyệt tay cộng
* amount/100, cùng một khoản ra hai kết quả lệch nhau 100 lần — nay đi chung một chỗ.
*/
public static function creditFor(int $amount): int
{
return (int) floor(max(0, $amount) / 100);
}
protected $fillable = [
'fullname',
'username',
@@ -291,6 +301,15 @@ class Customer extends Authenticatable implements JWTSubject
public function getJWTCustomClaims()
{
return [];
return ['pv' => $this->passwordVersion()];
}
/**
* Dấu vân tay của hash mật khẩu, nhúng vào JWT. Đổi/đặt lại mật khẩu → giá trị đổi →
* mọi access token cũ bị ApiAuthCheck từ chối (không cần blacklist).
*/
public function passwordVersion(): string
{
return substr(hash('sha256', (string) $this->password), 0, 16);
}
}
-97
View File
@@ -1,97 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EmailNotificationTemplate extends Model
{
protected $table = 'email_notification_templates';
protected $fillable = [
'name',
'type',
'subject',
'header_color',
'header_title',
'blocks',
'is_active',
];
protected $casts = [
'blocks' => 'array',
'is_active' => 'boolean',
];
/** Default blocks by template type */
public static function defaultBlocks(string $type = 'invoice_notification'): array
{
if ($type === 'vm_credentials') {
return [
['id' => 'greeting', 'type' => 'greeting', 'label' => 'Lời chào', 'enabled' => true, 'order' => 1, 'config' => []],
['id' => 'vm_credentials', 'type' => 'vm_credentials', 'label' => 'Thông tin truy cập VPS','enabled' => true, 'order' => 2, 'config' => [
'fields' => ['service_host', 'service_username', 'service_port', 'service_password', 'os_name'],
]],
['id' => 'product_info', 'type' => 'product_info', 'label' => 'Thông tin sản phẩm', 'enabled' => true, 'order' => 3, 'config' => [
'fields' => ['product_name', 'product_billing', 'product_price', 'service_due_exp'],
]],
['id' => 'custom_text', 'type' => 'custom_text', 'label' => 'Lời nhắn', 'enabled' => true, 'order' => 4, 'config' => [
'content' => 'Nếu bạn có bất kỳ thắc mắc nào, vui lòng liên hệ với chúng tôi để được hỗ trợ.',
]],
['id' => 'footer', 'type' => 'footer', 'label' => 'Footer', 'enabled' => true, 'order' => 5, 'config' => []],
];
}
// invoice_notification (default)
return [
['id' => 'greeting', 'type' => 'greeting', 'label' => 'Lời chào', 'enabled' => true, 'order' => 1, 'config' => []],
['id' => 'invoice_info', 'type' => 'invoice_info', 'label' => 'Thông tin hóa đơn', 'enabled' => true, 'order' => 2, 'config' => [
'fields' => ['invoice_number', 'created_at', 'due_date', 'total', 'status'],
]],
['id' => 'customer_info','type' => 'customer_info','label' => 'Thông tin khách hàng', 'enabled' => false, 'order' => 3, 'config' => [
'fields' => ['name', 'email'],
]],
['id' => 'order_items', 'type' => 'order_items', 'label' => 'Chi tiết sản phẩm', 'enabled' => true, 'order' => 4, 'config' => []],
['id' => 'attachments', 'type' => 'attachments', 'label' => 'File đính kèm', 'enabled' => true, 'order' => 5, 'config' => []],
['id' => 'custom_text', 'type' => 'custom_text', 'label' => 'Lời nhắn', 'enabled' => true, 'order' => 6, 'config' => [
'content' => 'Nếu bạn có bất kỳ thắc mắc nào, vui lòng liên hệ với chúng tôi để được hỗ trợ.',
]],
['id' => 'footer', 'type' => 'footer', 'label' => 'Footer', 'enabled' => true, 'order' => 7, 'config' => []],
];
}
/** Field definitions exposed per block type */
public static function blockFieldDefinitions(): array
{
return [
'invoice_info' => [
['key' => 'invoice_number', 'label' => 'Số hóa đơn'],
['key' => 'created_at', 'label' => 'Ngày tạo'],
['key' => 'due_date', 'label' => 'Hạn thanh toán'],
['key' => 'total', 'label' => 'Tổng tiền'],
['key' => 'sub_total', 'label' => 'Tiền dịch vụ'],
['key' => 'tax', 'label' => 'Thuế'],
['key' => 'status', 'label' => 'Trạng thái'],
['key' => 'payment_method', 'label' => 'Phương thức TT'],
],
'customer_info' => [
['key' => 'name', 'label' => 'Tên khách hàng'],
['key' => 'email', 'label' => 'Email'],
['key' => 'address', 'label' => 'Địa chỉ'],
],
'vm_credentials' => [
['key' => 'service_host', 'label' => 'Địa chỉ IP'],
['key' => 'service_username', 'label' => 'Username'],
['key' => 'service_port', 'label' => 'Port'],
['key' => 'service_password', 'label' => 'Mật khẩu'],
['key' => 'os_name', 'label' => 'Hệ điều hành'],
],
'product_info' => [
['key' => 'product_name', 'label' => 'Tên sản phẩm'],
['key' => 'product_billing', 'label' => 'Chu kỳ thanh toán'],
['key' => 'product_price', 'label' => 'Giá dịch vụ'],
['key' => 'service_due_exp', 'label' => 'Ngày hết hạn'],
],
];
}
}
+5 -7
View File
@@ -58,15 +58,13 @@ class Order extends Model
// Helpers
// -------------------------------------------------------------------------
/**
* orders.status là tinyInteger (OrderStatus), không phải chuỗi — bản cũ match 'pending'/
* 'completed'… nên mọi giá trị đều rơi vào default và hiển thị ra "0", "1", "2".
*/
public function getStatusLabelAttribute(): string
{
return match ($this->status) {
'pending' => 'Chờ xử lý',
'processing' => 'Đang xử lý',
'completed' => 'Hoàn tất',
'cancelled' => 'Đã hủy',
default => ucfirst($this->status),
};
return \App\Enums\Order\OrderStatus::tryFrom((int) $this->status)?->label() ?? 'Không xác định';
}
public function payment()
+12 -15
View File
@@ -2,6 +2,7 @@
namespace App\Models;
use App\Support\EmailTemplateShell;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
@@ -81,7 +82,6 @@ class Template extends Model
'vm_creation_failed' => 'Khởi tạo VPS thất bại',
'vm_password_changed' => 'Đổi mật khẩu VPS',
'vm_rebuild_failed' => 'Rebuild VPS thất bại',
'holiday_notice' => 'Thông báo nghỉ lễ',
'domain_order_approved' => 'Tên miền được duyệt',
'domain_order_rejected' => 'Tên miền bị từ chối',
'order_created' => 'Xác nhận đơn hàng',
@@ -98,8 +98,11 @@ class Template extends Model
'customer_signup_verify' => 'Xác thực đăng ký tài khoản',
'hosting_information' => 'Thông tin truy cập Hosting',
'customer_password_reset' => 'Khôi phục mật khẩu khách hàng',
'customer_password_reset_link' => 'Link đặt lại mật khẩu khách hàng',
'service_expiry' => 'Dịch vụ sắp hết hạn',
'customer_payment' => 'Thông báo thanh toán khách hàng',
'invoice_payment_reminder' => 'Nhắc thanh toán hoá đơn',
'invoice_notification' => 'Thông báo hoá đơn / gia hạn cước',
];
public function invoiceTemplate()
@@ -109,26 +112,20 @@ class Template extends Model
/**
* Wrap $contentHtml with the admin-editable "layout" template (scenario=layout),
* replacing its {{$content}} placeholder. Falls back to the static blade
* template-camp wrapper when no active layout template is configured.
* replacing its {{$content}} placeholder.
*
* Không còn fallback về blade: mọi email đều render từ tbl.template. Thiếu layout thì
* dùng khung header/footer chuẩn trong code thay vì gửi ra nội dung trần.
*/
public static function renderWithLayout(?string $subject, string $contentHtml): string
{
$layout = self::where('scenario', 'layout')->where('status', 1)->first();
if (!$layout || !$layout->message) {
return view('emails.customer.template-camp', [
'content' => $contentHtml,
'emailSubject' => $subject,
])->render();
}
$body = $layout && $layout->message
? str_replace('{{$content}}', $contentHtml, $layout->message)
: EmailTemplateShell::wrap(EmailTemplateShell::body($contentHtml));
$body = str_replace('{{$content}}', $contentHtml, $layout->message);
return view('emails.customer.template-envelope', [
'body' => $body,
'emailSubject' => $subject,
])->render();
return EmailTemplateShell::document($subject, $body);
}
public function type()
+1
View File
@@ -19,6 +19,7 @@ class Transaction extends Model
'public_code',
'trans',
'amount',
'received_amount',
'customer_id',
'user_id',
'note',
+1 -1
View File
@@ -64,7 +64,7 @@ class User extends Authenticatable implements JWTSubject
public function getNameAttribute(): string
{
return trim($this->firstname . ' ' . $this->lastname);
return trim((string) $this->fullname);
}
public function staffOnline()