Solutech Engineering
Innovations/Add-on Services

Payment Processing

Payment methods, transaction handling, and invoice generation

Payment Processing

The Add-on Services system supports multiple payment methods with comprehensive transaction handling and invoice generation.

Supported Payment Methods

1. MPesa (Mobile Money)

  • STK Push integration for seamless mobile payments
  • Real-time payment verification via transaction reference
  • Automatic retry mechanisms for failed transactions

2 Complimentary Credits

  • Admin-granted credits with proper audit trails
  • No payment required for promotional or support credits
  • Full transaction logging for compliance

Future Supported Payment Methods

3. Card Payments

  • Multiple gateway support (Flutterwave, Stripe, etc.)
  • Secure payment processing with PCI compliance
  • 3D Secure authentication for enhanced security

Payment Flow Architecture

sequenceDiagram
    participant User as User
    participant Frontend as Vue Component
    participant API as Payment Controller
    participant Service as Payment Service
    participant Gateway as Payment Gateway
    participant Credits as Credit Service

    User->>Frontend: Select plan & payment method
    Frontend->>API: POST /purchase-service-plan
    API->>Service: handle(request, user)
    
    alt MPesa Payment
        Service->>Gateway: initiateSTKPush()
        Gateway-->>Service: payment_reference
        Service-->>API: payment_initiated
        API-->>Frontend: payment_reference
        
        Frontend->>API: POST /verify-purchase-service-plan
        API->>Service: verify(payment_reference)
        Service->>Gateway: queryTransaction(reference)
        Gateway-->>Service: payment_status
    end
    
    alt Payment Successful
        Service->>Credits: addCreditsToAccount()
        Credits-->>Service: credits_added
        Service-->>API: payment_completed
        API-->>Frontend: success_response
    end

Payment Controllers

AddOnServicesPaymentController

Main controller handling payment requests from the frontend.

<?php

namespace App\Http\Controllers\V2\Controllers\Services;

class AddOnServicesPaymentController extends Controller
{
    public function store(StoreRequest $request): JsonResponse
    {
        $data = (object) $request->validated();
        $user = $request->user();

        // Process payment through service
        $result = $this->storeAddonService->handle($data, $user);

        if (!$result['status']) {
            throw new \Exception($result['message'], 400);
        }

        return $this->success($result['data'], $result['message'], 201);
    }

    public function verify(VerifyRequest $request): JsonResponse
    {
        $data = (object) $request->validated();
        $user = $request->user();

        // Verify payment through service
        $result = $this->storeAddonService->verify($data, $user, $servicePayment);

        return $this->success($result, 'Payment verified successfully');
    }
}

BillingAddOnsPaymentController

Administrative controller for payment management and reporting.

<?php

namespace App\Http\Controllers\V2\Controllers\AddonServices;

class BillingAddOnsPaymentController extends Controller
{
    public function index(): JsonResponse
    {
        // List payments with filtering and pagination
        $payments = BillingAddOnsPayment::with(['plan', 'user'])
            ->when(request('status'), function ($query, $status) {
                return $query->where('invoice_status', $status);
            })
            ->when(request('user_id'), function ($query, $userId) {
                return $query->where('user_id', $userId);
            })
            ->orderBy('created_at', 'desc')
            ->paginate(50);

        return response()->json($payments);
    }

    public function generateInvoice(GenerateInvoiceRequest $request): JsonResponse
    {
        // Generate invoice for payment
        $invoice = $this->invoiceService->generate($request->validated());

        return $this->success($invoice, 'Invoice generated successfully');
    }
}

Payment Service Implementation

StoreAddOnServicesPaymentTransactionService

Core service handling payment transactions.

<?php

namespace App\Services\AddOnServices\Payments;

class StoreAddOnServicesPaymentTransactionService
{
    public function handle(object $request, User $user): array
    {
        // Validate payment action and user permissions
        $this->validatePaymentRequest($request, $user);

        // Get selected plan details
        $plan = BillingAddOnsPlan::findOrFail($request->billing_add_ons_plans_id);

        try {
            DB::beginTransaction();

            // Create payment record
            $payment = $this->createPaymentRecord($request, $plan, $user);

            // Process payment based on method
            $result = $this->processPaymentByMethod($request, $payment, $user);

            DB::commit();

            return [
                'status' => true,
                'data' => $result,
                'message' => 'Payment initiated successfully'
            ];

        } catch (Exception $e) {
            DB::rollBack();
            Log::error('Payment processing failed', [
                'user_id' => $user->id,
                'plan_id' => $request->billing_add_ons_plans_id,
                'error' => $e->getMessage()
            ]);
            throw $e;
        }
    }

    public function verify(object $request, User $user, BillingAddOnsPlan $servicePlan): BillingAddOnsPayment
    {
        // Verify payment with gateway
        $paymentStatus = $this->verifyWithGateway($request->payment_reference);

        if ($paymentStatus['success']) {
            // Update payment status
            $payment = BillingAddOnsPayment::where('payment_reference', $request->payment_reference)
                ->firstOrFail();

            $payment->update([
                'invoice_status' => 'Closed',
                'verified_at' => now()
            ]);

            // Allocate credits
            $this->allocateCredits($payment, $servicePlan);

            return $payment;
        }

        throw new PaymentVerificationException('Payment verification failed');
    }

    private function processPaymentByMethod(object $request, BillingAddOnsPayment $payment, User $user)
    {
        switch ($request->payment_method) {
            case 'mpesa':
                return $this->processMpesaPayment($request, $payment);
            
            case 'card':
                return $this->processCardPayment($request, $payment);
            
            case 'complimentary':
                return $this->processComplimentaryPayment($payment, $user);
            
            default:
                throw new InvalidPaymentMethodException('Unsupported payment method');
        }
    }
}

MPesa Integration

STK Push Implementation

<?php

namespace App\Services\Payments;

class MpesaPaymentService
{
    public function initiateStkPush($amount, $phoneNumber, $reference): array
    {
        $endpoint = config('mpesa.base_url') . '/mpesa/stkpush/v1/processrequest';
        
        $payload = [
            'BusinessShortCode' => config('mpesa.shortcode'),
            'Password' => $this->generatePassword(),
            'Timestamp' => $this->getTimestamp(),
            'TransactionType' => 'CustomerPayBillOnline',
            'Amount' => $amount,
            'PartyA' => $phoneNumber,
            'PartyB' => config('mpesa.shortcode'),
            'PhoneNumber' => $phoneNumber,
            'CallBackURL' => route('mpesa.callback'),
            'AccountReference' => $reference,
            'TransactionDesc' => 'Add-on Services Payment'
        ];
        
        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . $this->getAccessToken(),
            'Content-Type' => 'application/json'
        ])->post($endpoint, $payload);
        
        if ($response->successful()) {
            $data = $response->json();
            return [
                'success' => true,
                'checkout_request_id' => $data['CheckoutRequestID'],
                'merchant_request_id' => $data['MerchantRequestID'],
                'response_code' => $data['ResponseCode']
            ];
        }
        
        throw new MpesaException('STK Push failed: ' . $response->body());
    }

    public function verifyTransaction($checkoutRequestId): array
    {
        $endpoint = config('mpesa.base_url') . '/mpesa/stkpushquery/v1/query';
        
        $payload = [
            'BusinessShortCode' => config('mpesa.shortcode'),
            'Password' => $this->generatePassword(),
            'Timestamp' => $this->getTimestamp(),
            'CheckoutRequestID' => $checkoutRequestId
        ];
        
        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . $this->getAccessToken(),
            'Content-Type' => 'application/json'
        ])->post($endpoint, $payload);
        
        if ($response->successful()) {
            $data = $response->json();
            return [
                'success' => $data['ResultCode'] === '0',
                'result_desc' => $data['ResultDesc'],
                'amount' => $data['Amount'] ?? null,
                'mpesa_receipt' => $data['MpesaReceiptNumber'] ?? null
            ];
        }
        
        throw new MpesaException('Transaction verification failed: ' . $response->body());
    }

    private function generatePassword(): string
    {
        $shortcode = config('mpesa.shortcode');
        $passkey = config('mpesa.passkey');
        $timestamp = $this->getTimestamp();
        
        return base64_encode($shortcode . $passkey . $timestamp);
    }

    private function getTimestamp(): string
    {
        return date('YmdHis');
    }

    private function getAccessToken(): string
    {
        // Cache access token for 1 hour
        return Cache::remember('mpesa_access_token', 3600, function () {
            return $this->requestAccessToken();
        });
    }
}

Invoice Generation System

<?php

namespace App\Services\AddOnServices\Payments;

class GenerateAddOnInvoiceService
{
    public function handle(object $request): array
    {
        try {
            DB::beginTransaction();

            // Step 1: Validate request and check for duplicates
            $this->validateInvoiceRequest($request);

            // Step 2: Get plan and create payment record
            $plan = $this->getAddOnPlan($request);
            $payment = $this->createPaymentRecord($request, $plan);

            // Step 3: Generate invoice through external service
            $invoiceData = $this->generateInvoiceDocument($payment, $plan);

            // Step 4: Update payment with invoice details
            $payment->update([
                'invoice_number' => $invoiceData['invoice_number'],
                'invoice_document' => $invoiceData['document_path'],
                'invoiced_at' => now()
            ]);

            DB::commit();

            return [
                'success' => true,
                'invoice_number' => $invoiceData['invoice_number'],
                'document_path' => $invoiceData['document_path']
            ];

        } catch (Exception $e) {
            DB::rollBack();
            Log::error('Invoice generation failed', [
                'request' => $request,
                'error' => $e->getMessage()
            ]);
            throw $e;
        }
    }

    private function generateInvoiceDocument($payment, $plan): array
    {
        $invoiceController = new InvoiceController();
        
        $invoiceData = [
            'invoice_type' => 'proforma',
            'client_id' => $payment->user->client_id,
            'invoice_date' => now()->format('Y-m-d'),
            'due_date' => now()->addDays(30)->format('Y-m-d'),
            'discount_percent' => 0,
            'status_id' => Status::where('name', 'Pending')->firstOrFail()->id,
            'sync_to_kra' => false,
            'adjustment_amount' => 0,
            'custom_subject' => "Add-on Services Invoice",
            'invoice_items' => [
                [
                    'product_item_id' => $plan->product_item_id,
                    'qty' => $payment->credits,
                    'unit_price' => $payment->amount / $payment->credits,
                    'description' => "{$plan->name} - {$payment->credits} credits",
                    'tax_rate' => 0
                ]
            ]
        ];

        $request = new Request($invoiceData);
        $response = $invoiceController->store($request);
        
        return json_decode($response->getContent(), true);
    }
}

This comprehensive payment processing system ensures secure, reliable handling of transactions across multiple payment methods while maintaining proper audit trails and error handling.