+3

Xây dựng module thanh toán COD chuẩn Enterprise: Từ Init Project đến test Postman

Lời mở đầu

Nhiều anh em khi nhận task làm tính năng thanh toán COD thường viết tuột mọi thứ vào Controller: Nhận request -> Lưu DB -> Trả response. Nhưng khi dự án phình to, sếp yêu cầu tích hợp thêm Momo, ZaloPay, thẻ tín dụng... cái Controller đó sẽ biến thành một bãi lầy if/else không ai dám đụng.

Hôm nay, chúng ta sẽ cùng nhau xây dựng luồng thanh toán COD từ con số 0 trong Laravel. Nhưng không phải code theo kiểu "chạy được là xong", mà chúng ta sẽ ép nó vào kiến trúc Strategy Pattern và Service Pattern để code sạch ngay từ dòng đầu tiên.

Bước 1: Khởi tạo dự án và Database (Chuẩn bị "móng")

Đầu tiên, tạo một project Laravel mới (nếu bạn chưa có):

laravel new enterprise-payment
cd enterprise-payment

Tạo Model và Migration cho thực thể Order (Đơn hàng):

php artisan make:model Order -m

Mở file migration vừa tạo lên và định nghĩa các trường cơ bản:

// database/migrations/xxxx_create_orders_table.php
public function up(): void
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->string('order_code')->unique();
        $table->decimal('total_amount', 15, 2);
        $table->string('payment_method'); // Ví dụ: cod, vnpay, momo
        $table->string('payment_status'); // Ví dụ: pending, paid, failed
        $table->string('order_status'); // Ví dụ: new, processing, completed
        $table->timestamps();
    });
}

Chạy migration để tạo bảng:

php artisan migrate

(Đừng quên cấu hình kết nối Database trong file .env nhé).

Bước 2: Thiết lập Strategy Pattern cho Payment

Đây là linh hồn của hệ thống. Thay vì viết logic thanh toán cứng vào Controller, ta định nghĩa một "bản hợp đồng" (Interface).

1. Tạo Interface PaymentStrategy

Tạo thư mục app/Strategies/Payment/ và thêm file:

// app/Strategies/Payment/PaymentStrategyInterface.php
namespace App\Strategies\Payment;

use App\Models\Order;

interface PaymentStrategyInterface
{
    /**
     * Xử lý logic thanh toán cho đơn hàng
     * Trả về mảng thông tin response cho Client
     */
    public function processPayment(Order $order): array;
}

2. Tạo class CodPaymentStrategy

COD thì không cần gọi API ngân hàng, logic của nó chủ yếu là đánh dấu trạng thái đơn hàng và trả về thông báo thành công.

// app/Strategies/Payment/CodPaymentStrategy.php
namespace App\Strategies\Payment;

use App\Models\Order;

class CodPaymentStrategy implements PaymentStrategyInterface
{
    public function processPayment(Order $order): array
    {
        // Với COD, lúc vừa đặt hàng xong thì chưa thu tiền
        $order->update([
            'payment_status' => 'pending', 
            'order_status' => 'processing',
        ]);

        // Trả về format chuẩn cho Frontend
        return [
            'success' => true,
            'message' => 'Đặt hàng COD thành công. Vui lòng thanh toán khi nhận hàng.',
            'data' => [
                'order_code' => $order->order_code,
                'total_amount' => $order->total_amount,
                'redirect_url' => '/checkout/success' // URL để FE chuyển trang
            ]
        ];
    }
}

Tầm nhìn: Sau này nếu có VNPay, bạn chỉ cần tạo class VnPayPaymentStrategy, gọi API sinh link thanh toán và trả cái link đó vào redirect_url là xong! Code cực kỳ tách biệt.

Bước 3: Tạo Service Context để quản lý Strategy

Controller không nên biết cách gọi Strategy như thế nào. Ta tạo một Service làm trung gian.

php artisan make:service PaymentService

(Nếu Laravel chưa có lệnh make:service, bạn tự tạo file thủ công nhé).

// app/Services/PaymentService.php
namespace App\Services;

use App\Models\Order;
use App\Strategies\Payment\PaymentStrategyInterface;
use App\Strategies\Payment\CodPaymentStrategy;
// use App\Strategies\Payment\VnPayPaymentStrategy; 
use Exception;

class PaymentService
{
    protected PaymentStrategyInterface $strategy;

    // Factory method đơn giản để chọn Strategy dựa trên string
    public function setStrategy(string $paymentMethod): self
    {
        $this->strategy = match ($paymentMethod) {
            'cod' => new CodPaymentStrategy(),
            // 'vnpay' => new VnPayPaymentStrategy(),
            default => throw new Exception("Phương thức thanh toán {$paymentMethod} không được hỗ trợ."),
        };

        return $this;
    }

    public function process(Order $order): array
    {
        if (!isset($this->strategy)) {
            throw new Exception("Chưa thiết lập Payment Strategy.");
        }

        return $this->strategy->processPayment($order);
    }
}

Bước 4: Viết Controller và Route

Bây giờ hãy xem cái Controller của chúng ta "gầy" và đẹp như thế nào.

php artisan make:controller Api/OrderController
// app/Http/Controllers/Api/OrderController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Services\PaymentService;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class OrderController extends Controller
{
    protected PaymentService $paymentService;

    // Inject Service vào Controller
    public function __construct(PaymentService $paymentService)
    {
        $this->paymentService = $paymentService;
    }

    public function checkout(Request $request)
    {
        // 1. Validate request
        $request->validate([
            'total_amount' => 'required|numeric|min:0',
            'payment_method' => 'required|string|in:cod,vnpay,momo',
        ]);

        try {
            // 2. Tạo đơn hàng tạm thời (Trạng thái new)
            $order = Order::create([
                'order_code' => 'ORD-' . strtoupper(Str::random(8)),
                'total_amount' => $request->total_amount,
                'payment_method' => $request->payment_method,
                'payment_status' => 'pending',
                'order_status' => 'new',
            ]);

            // 3. Xử lý thanh toán qua Service
            $result = $this->paymentService
                ->setStrategy($request->payment_method)
                ->process($order);

            return response()->json($result, 200);

        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => $e->getMessage()
            ], 400);
        }
    }
}

Mở file routes/api.php và đăng ký endpoint:

use App\Http\Controllers\Api\OrderController;

Route::post('/checkout', [OrderController::class, 'checkout']);

Bước 5: Thử lửa với Postman

Khởi động server Laravel:

php artisan serve

Mặc định server sẽ chạy ở [http://127.0.0.1:8000](http://127.0.0.1:8000).

Mở Postman lên và thiết lập:

  1. Method: POST
  2. URL: [http://127.0.0.1:8000/api/checkout](http://127.0.0.1:8000/api/checkout)
  3. Headers:
  • Accept: application/json
  1. Body (chọn tab raw -> format JSON):
{
    "total_amount": 500000,
    "payment_method": "cod"
}

Nhấn SEND và tận hưởng thành quả (Expected Response):

{
    "success": true,
    "message": "Đặt hàng COD thành công. Vui lòng thanh toán khi nhận hàng.",
    "data": {
        "order_code": "ORD-XYZ123AB",
        "total_amount": 500000,
        "redirect_url": "/checkout/success"
    }
}

Nếu bạn thử truyền "payment_method": "paypal", hệ thống sẽ lập tức Validate lỗi hoặc văng Exception do chúng ta chưa implement Strategy đó, bảo vệ hệ thống chặt chẽ.

Tóm lại

Bạn thấy đó, chỉ là một luồng COD rất cơ bản, nhưng khi chúng ta áp dụng Strategy Pattern, cấu trúc thư mục trở nên rành mạch:

  • Controller chỉ làm nhiệm vụ nhận Request và trả Response.
  • Service lo điều phối luồng chạy.
  • Strategy cô lập hoàn toàn logic riêng biệt của từng cổng thanh toán.

Viết code như vậy, đến lúc scale dự án hay bàn giao cho người khác, anh em hoàn toàn có thể kê cao gối ngủ ngon. Hãy thử clone kiến trúc này vào dự án của bạn và cảm nhận sự khác biệt nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí