DEV Community

Play Button Pause Button
katorymnddev
katorymnddev

Posted on

Exploring Pawapay Deposits, Refunds, and Payouts: A Hands-On Preview

Exploring Pawapay Deposits, Refunds, and Payouts: A Hands-On Preview

Pawapay Integration

Hello everyone! πŸ‘‹

I'm excited to share a sneak peek of our latest integration with Pawapayβ€”a mobile payment solution that streamlines deposits, refunds, and payouts. In the accompanying video, you'll see these features in action, including validation processes and the success responses from the Pawapay Mobile API.

But first, let's dive into what Pawapay offers and how it can elevate your application's payment capabilities.

πŸ“– What is Pawapay?

Pawapay is a leading mobile money payment gateway in Africa, enabling businesses to transact seamlessly across various mobile networks and countries. With a single API, you can:

  • Accept deposits from users' mobile money accounts.
  • Process refunds efficiently.
  • Initiate payouts to users or vendors.

πŸš€ Getting Started with Pawapay Integration

Before we jump into the functionalities, ensure you've set up your Pawapay account and obtained your API credentials.

# Install the pawaPay PHP SDK via composer
composer require katorymnd/pawa-pay-integration
Enter fullscreen mode Exit fullscreen mode

Authentication

Authenticate your API requests using your API key:

// Set the environment and SSL verification based on the production status
$environment = getenv('ENVIRONMENT') ?: 'sandbox'; // Default to sandbox if not specified
$sslVerify = $environment === 'production';  // SSL verification true in production

// Dynamically construct the API token key
$apiTokenKey = 'PAWAPAY_' . strtoupper($environment) . '_API_TOKEN';

// Get the API token based on the environment
$apiToken = $_ENV[$apiTokenKey] ?? null;
Enter fullscreen mode Exit fullscreen mode

πŸ’° Deposits

Depositing funds into your application is straightforward.

// Create a new instance of the API client with SSL verification control
$pawaPayClient = new ApiClient($apiToken, $environment, $sslVerify);

// Generate a unique deposit ID using a helper method (UUID v4)
$depositId = Helpers::generateUniqueId();

// Prepare metadata if needed (up to 10 items allowed)
$metadata = [];

try {
    // Step 1: Validate the amount using Symfony validation and custom validation
    $validatedAmount = Validator::symfonyValidateAmount($amount);  // Symfony Validator for amount

    // Step 2: Use the Validator to check if the description is valid (alphanumeric and length)
    $validatedDescription = Validator::validateStatementDescription($description);

    // Step 3: Validate the number of metadata items only if metadata is provided
    if (!empty($metadata)) {
        Validator::validateMetadataItemCount($metadata);
    }

    // Step 4: Initiate the deposit using the submitted details
    $response = $pawaPayClient->initiateDeposit(
        $depositId,
        $validatedAmount,
        $currency,
        $mno,
        $payerMsisdn,
        $validatedDescription,
        $metadata
    );

    if ($response['status'] === 200) {
        // Log initiation success
        $log->info('Deposit initiated successfully', [
            'depositId' => $depositId,
            'response' => $response['response']
        ]);

        // Proceed to check the transaction status
        $statusResponse = $pawaPayClient->checkTransactionStatus($depositId, 'deposit');

        if ($statusResponse['status'] === 200) {
            $depositInfo = $statusResponse['response'][0]; // Get the deposit info
            $depositStatus = $depositInfo['status'];

            if ($depositStatus === 'COMPLETED') {
                // Log successful deposit
                $log->info('Deposit completed successfully', [
                    'depositId' => $depositId,
                    'response' => $depositInfo
                ]);

                // Send success response back to JavaScript
                echo json_encode([
                    'success' => true,
                    'transactionId' => $depositId,
                    'message' => 'Payment processed successfully.'
                ]);
            } elseif ($depositStatus === 'FAILED') {
                // Deposit failed
                $failureReason = $depositInfo['failureReason'];
                $failureCode = $failureReason['failureCode'];
                $failureMessage = FailureCodeHelper::getFailureMessage($failureCode);

                $log->error('Deposit failed', [
                    'depositId' => $depositId,
                    'failureCode' => $failureCode,
                    'failureMessage' => $failureMessage,
                    'response' => $depositInfo
                ]);

                // Send error response back to JavaScript
                echo json_encode([
                    'success' => false,
                    'errorMessage' => 'Payment failed: ' . $failureMessage
                ]);
            } else {
                // Deposit is pending or in another state
                $log->info('Deposit is in state: ' . $depositStatus, [
                    'depositId' => $depositId,
                    'response' => $depositInfo
                ]);

                // Send pending response back to JavaScript
                echo json_encode([
                    'success' => false,
                    'errorMessage' => 'Payment is processing. Please wait and check your account.'
                ]);
            }
        } else {
            // Failed to retrieve deposit status
            $log->error('Failed to retrieve deposit status', [
                'depositId' => $depositId,
                'response' => $statusResponse
            ]);
            echo json_encode([
                'success' => false,
                'errorMessage' => 'Unable to retrieve deposit status.'
            ]);
        }
    } else {
        // Log initiation failure
        $log->error('Deposit initiation failed', [
            'depositId' => $depositId,
            'response' => $response
        ]);

        // Send error response back to JavaScript
        echo json_encode([
            'success' => false,
            'errorMessage' => 'Payment initiation failed: ' . $response['response']['message']
        ]);
    }
} catch (Exception $e) {
    // Catch validation errors and display the message
    $errorMessage = "Validation Error: " . $e->getMessage();

    // Log the validation error
    $log->error('Validation error occurred', [
        'depositId' => $depositId,
        'error' => $errorMessage
    ]);

    // Send error response back to JavaScript
    echo json_encode([
        'success' => false,
        'errorMessage' => $errorMessage
    ]);
}

Enter fullscreen mode Exit fullscreen mode

What happens here?

  • Validation: The API validates the phone number and amount.
  • Initiation: A deposit request is sent to the user's mobile money account.
  • Response: You receive a transaction ID and status.

πŸ”„ Refunds

Processing refunds is just as simple.

// Create a new instance of the API client with SSL verification control
$pawaPayClient = new ApiClient($apiToken, $environment, $sslVerify);

// Retrieve and sanitize form data
$depositId = isset($_POST['depositId']) ? trim($_POST['depositId']) : '';
$amount = isset($_POST['amount']) ? trim($_POST['amount']) : '';

// Retrieve metadata fields from the form
$metadataFieldNames = isset($_POST['metadataFieldName']) ? $_POST['metadataFieldName'] : [];
$metadataFieldValues = isset($_POST['metadataFieldValue']) ? $_POST['metadataFieldValue'] : [];
$metadataIsPII = isset($_POST['metadataIsPII']) ? $_POST['metadataIsPII'] : [];

// Ensure that metadata fields are arrays
$metadataFieldNames = is_array($metadataFieldNames) ? $metadataFieldNames : [$metadataFieldNames];
$metadataFieldValues = is_array($metadataFieldValues) ? $metadataFieldValues : [$metadataFieldValues];
$metadataIsPII = is_array($metadataIsPII) ? $metadataIsPII : [$metadataIsPII];

// Construct metadata array
$metadata = [];
for ($i = 0; $i < count($metadataFieldNames); $i++) {
    $fieldName = trim($metadataFieldNames[$i]);
    $fieldValue = trim($metadataFieldValues[$i]);
    $isPII = isset($metadataIsPII[$i]) ? true : false;

    // Skip empty metadata fields
    if ($fieldName === '' || $fieldValue === '') {
        continue;
    }

    $metadataItem = [
        'fieldName' => $fieldName,
        'fieldValue' => $fieldValue,
    ];

    if ($isPII) {
        $metadataItem['isPII'] = true;
    }

    $metadata[] = $metadataItem;
}

// Function to validate UUID (version 4)
function isValidUUIDv4($uuid)
{
    return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $uuid);
}

// Function to validate email
function isValidEmail($email)
{
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

// Prepare the request payload (keeping it as provided)
$refundId = Helpers::generateUniqueId();  // Generate a valid UUID for the refundId
// Note: The depositId is obtained from the form
// The amount is obtained from the form
// Metadata is constructed from the form inputs

try {
    // Validate required fields
    if (empty($depositId) || empty($amount)) {
        throw new Exception('Missing required fields: Deposit ID and Refund Amount are required.');
    }

    // Validate Deposit ID
    if (!isValidUUIDv4($depositId)) {
        throw new Exception('Invalid Deposit ID format. It must be a valid UUID version 4.');
    }

    // Validate Refund Amount
    if (!is_numeric($amount) || floatval($amount) <= 0) {
        throw new Exception('Invalid Refund Amount. It must be a positive number.');
    }

    // Validate Metadata
    // Ensure predefined metadata fields are modified
    $predefinedMetadata = [
        'orderId' => 'ORD-123456789',
        'customerId' => 'customer@email.com'
    ];

    foreach ($metadata as $meta) {
        if (array_key_exists($meta['fieldName'], $predefinedMetadata)) {
            if ($meta['fieldValue'] === $predefinedMetadata[$meta['fieldName']]) {
                throw new Exception("Please update the pre-filled {$meta['fieldName']} value before submitting.");
            }

            // Additional validation for customerId
            if ($meta['fieldName'] === 'customerId' && !isValidEmail($meta['fieldValue'])) {
                throw new Exception("Please enter a valid email address for customerId.");
            }
        }
    }

    // Validate metadata count (maximum 10)
    if (count($metadata) > 10) {
        throw new Exception('You cannot add more than 10 metadata fields.');
    }

    // Prepare the data payload
    $data = [
        'refundId' => $refundId,
        'depositId' => $depositId,
        'amount' => floatval($amount),
        'metadata' => $metadata
    ];

    // Initiate the refund using ApiClient
    $response = $pawaPayClient->initiateRefund($refundId, $depositId, floatval($amount), $metadata);

    // Check the API response status
    if ($response['status'] === 200) {
        // Log refund initiation success
        $log->info('Refund initiated successfully', [
            'refundId' => $refundId,
            'depositId' => $depositId,
            'amount' => $amount,
            'metadata' => $metadata,
            'response' => $response['response']
        ]);

        // Now check the refund status
        try {
            $statusResponse = $pawaPayClient->checkTransactionStatus($refundId, 'refund');

            if ($statusResponse['status'] === 200) {
                // Log refund status retrieval success
                $log->info('Refund status retrieved successfully', [
                    'refundId' => $refundId,
                    'statusResponse' => $statusResponse['response']
                ]);

                // Send success response back to the client with refund status
                echo json_encode([
                    'success' => true,
                    'refundId' => $refundId,
                    'message' => 'Refund initiated and status retrieved successfully.',
                    'refundStatus' => $statusResponse['response']
                ]);
            } else {
                // Log failure to retrieve refund status
                $log->error('Failed to retrieve refund status', [
                    'refundId' => $refundId,
                    'statusResponse' => $statusResponse
                ]);

                // Send response indicating refund initiation success but unable to retrieve status
                echo json_encode([
                    'success' => true,
                    'refundId' => $refundId,
                    'message' => 'Refund initiated successfully, but unable to retrieve refund status.',
                    'error' => 'Unable to retrieve refund status.'
                ]);
            }
        } catch (Exception $e) {
            // Log the error
            $log->error('Error occurred while checking refund status', [
                'refundId' => $refundId,
                'error' => $e->getMessage()
            ]);

            // Send response indicating refund initiation success but error in retrieving status
            echo json_encode([
                'success' => true,
                'refundId' => $refundId,
                'message' => 'Refund initiated successfully, but an error occurred while retrieving refund status.',
                'error' => $e->getMessage()
            ]);
        }
    } else {
        // Log initiation failure
        $log->error('Refund initiation failed', [
            'refundId' => $refundId,
            'depositId' => $depositId,
            'amount' => $amount,
            'metadata' => $metadata,
            'response' => $response
        ]);

        // Extract error message from response
        $errorMessage = isset($response['response']['message']) ? $response['response']['message'] : 'Unknown error occurred.';
        echo json_encode([
            'success' => false,
            'errorMessage' => 'Refund initiation failed: ' . $errorMessage
        ]);
    }
} catch (Exception $e) {
    // Catch validation errors and return the message
    $errorMessage = $e->getMessage();

    // Log the error
    $log->error('Error occurred during refund initiation', [
        'refundId' => $refundId,
        'depositId' => $depositId,
        'amount' => $amount,
        'metadata' => $metadata,
        'error' => $errorMessage
    ]);

    // Send error response back to the client
    echo json_encode([
        'success' => false,
        'errorMessage' => $errorMessage
    ]);
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Partial Refunds: You can refund any amount up to the original transaction amount.
  • Validation: The API checks if the transaction is eligible for a refund.

πŸ“€ Payouts

Send payouts to users or vendors effortlessly.

// Create a new instance of the API client with SSL verification control
$pawaPayClient = new ApiClient($apiToken, $environment, $sslVerify);

// Get the raw POST data (JSON) sent from the JavaScript fetch request
$data = json_decode(file_get_contents('php://input'), true);

if ($data) {
    // Access the form data in the $data array
    $recipientsData = isset($data['recipients']) ? $data['recipients'] : [];

    $responses = []; // To collect responses for each recipient

    foreach ($recipientsData as $index => $recipientData) {
        try {
            // Prepare recipient data
            $recipient = [
                'payoutId' => Helpers::generateUniqueId(), // Generate a unique payout ID for each recipient
                'amount' => $recipientData['amount'],
                'currency' => $recipientData['currency'],
                'correspondent' => $recipientData['correspondent'],
                'recipientMsisdn' => $recipientData['recipientMsisdn'],
                'statementDescription' => $recipientData['statementDescription'],
            ];

            // Prepare metadata if available
            $metadata = isset($recipientData['metadata']) ? $recipientData['metadata'] : [];

            // Validate the amount using Symfony validation
            $validatedAmount = Validator::symfonyValidateAmount($recipient['amount']);

            // Validate the description for each recipient
            $validatedDescription = Validator::validateStatementDescription($recipient['statementDescription']);

            // Initiate the payout for each recipient
            $initiateResponse = $pawaPayClient->initiatePayout(
                $recipient['payoutId'],
                $validatedAmount,
                $recipient['currency'],
                $recipient['correspondent'],
                $recipient['recipientMsisdn'],
                $validatedDescription,
                $metadata
            );

            // Simulate a short delay
            sleep(2); // Reduced delay for better performance

            // Check the payout status
            $statusResponse = $pawaPayClient->checkTransactionStatus($recipient['payoutId'], 'payout');

            // Prepare the final response based on the payout status
            if ($statusResponse['status'] === 200 && isset($statusResponse['response'][0]['status']) && $statusResponse['response'][0]['status'] === 'COMPLETED') {
                // Payout completed successfully
                $responses[] = [
                    'recipientMsisdn' => $recipient['recipientMsisdn'],
                    'success' => true,
                    'details' => sprintf(
                        'Payout of %s %s to %s was completed successfully with Payout ID: %s.',
                        $validatedAmount,
                        $recipient['currency'],
                        $recipient['recipientMsisdn'],
                        $recipient['payoutId']
                    ),
                    'response' => $statusResponse['response']
                ];
                $log->info('Payout completed successfully', [
                    'payoutId' => $recipient['payoutId'],
                    'response' => $statusResponse['response']
                ]);
            } else {
                // Payout failed
                $failureReason = $statusResponse['response'][0]['failureReason']['failureMessage'] ?? 'Unknown error';
                $responses[] = [
                    'recipientMsisdn' => $recipient['recipientMsisdn'],
                    'success' => false,
                    'details' => sprintf(
                        'Payout of %s %s to %s failed with Payout ID: %s. Reason: %s.',
                        $validatedAmount,
                        $recipient['currency'],
                        $recipient['recipientMsisdn'],
                        $recipient['payoutId'],
                        $failureReason
                    ),
                    'error' => $failureReason
                ];
                $log->error('Payout failed', [
                    'payoutId' => $recipient['payoutId'],
                    'error' => $failureReason
                ]);
            }

        } catch (Exception $e) {
            // Catch validation errors and display the message
            $responses[] = [
                'recipientMsisdn' => $recipientData['recipientMsisdn'],
                'success' => false,
                'details' => sprintf(
                    'Payout of %s %s to %s failed during processing. Reason: %s.',
                    $recipientData['amount'],
                    $recipientData['currency'],
                    $recipientData['recipientMsisdn'],
                    $e->getMessage()
                ),
                'error' => $e->getMessage()
            ];
            // Log the error if payoutId is set, else log with recipient info
            $log->error('Payout processing error', [
                'recipientMsisdn' => $recipientData['recipientMsisdn'],
                'error' => $e->getMessage()
            ]);
        }
    }

    // Summarize the outcome of all payout attempts
    $successfulCount = count(array_filter($responses, fn ($item) => $item['success']));
    $failedCount = count($responses) - $successfulCount;

    // Return the responses as JSON
    $response = [
        'success' => $failedCount === 0,
        'message' => sprintf(
            'Payout processing completed. %d successful and %d failed.',
            $successfulCount,
            $failedCount
        ),
        'responses' => $responses,
        'total_recipients' => count($recipientsData)
    ];

    echo json_encode($response, JSON_PRETTY_PRINT);

} else {
    // Handle the error (if no data received)
    $response = [
        'success' => false,
        'message' => 'No data received.'
    ];
    echo json_encode($response, JSON_PRETTY_PRINT);
}
Enter fullscreen mode Exit fullscreen mode

Notes:

  • Multiple Currencies: Support for various African currencies.
  • Compliance: Automatically handles compliance and regulatory requirements.

βœ… Success Responses

Understanding the API responses is crucial for seamless integration.

Deposit Success Response:

 // Send success response back to JavaScript
                echo json_encode([
                    'success' => true,
                    'transactionId' => $depositId,
                    'message' => 'Payment processed successfully.'
                ]);
Enter fullscreen mode Exit fullscreen mode

Refund Success Response:

 // Send success response back to the client with refund status
                echo json_encode([
                    'success' => true,
                    'refundId' => $refundId,
                    'message' => 'Refund initiated and status retrieved successfully.',
                    'refundStatus' => $statusResponse['response']
                ]);
Enter fullscreen mode Exit fullscreen mode

Payout Success Response:

// Summarize the outcome of all payout attempts
    $successfulCount = count(array_filter($responses, fn ($item) => $item['success']));
    $failedCount = count($responses) - $successfulCount;

    // Return the responses as JSON
    $response = [
        'success' => $failedCount === 0,
        'message' => sprintf(
            'Payout processing completed. %d successful and %d failed.',
            $successfulCount,
            $failedCount
        ),
        'responses' => $responses,
        'total_recipients' => count($recipientsData)
    ];

    echo json_encode($response, JSON_PRETTY_PRINT);
Enter fullscreen mode Exit fullscreen mode

These responses help you update your application's state and notify users accordingly.

πŸŽ₯ Watch It in Action!

The video at the top showcases all these features with live validation and API interactions. You'll see how deposits, refunds, and payouts work seamlessly, along with the success responses from Pawapay.

πŸ”— Useful Links

🌟 Conclusion

Integrating Pawapay opens up a world of possibilities for handling mobile money transactions in your application. I hope this post and the accompanying video provide valuable insights into what you can achieve.

Feel free to reach out or leave a commentβ€”let's build something amazing together!

Top comments (0)