> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moduluslabs.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing in Sandbox

> Learn how to test Dynamic QR Ph transactions in the sandbox environment

## Overview

The Modulus Labs Sandbox environment allows you to test your QR Ph integration without processing real payments. The sandbox mirrors production behavior, making it perfect for development and testing.

<Note>
  **Sandbox vs. Production:** The sandbox environment uses separate credentials and URLs. No real money changes hands in sandbox mode.
</Note>

## Prerequisites

Before testing, ensure you have:

<Steps>
  <Step title="Sandbox Credentials">
    * Secret Key (for authentication)
    * Encryption Key (for JWE tokens)
    * Activation Code (for your sub-merchant account)
  </Step>

  <Step title="Webhook Integration">
    Register a webhook endpoint using the Modulus Labs Webhooks API to receive payment notifications
  </Step>

  <Step title="Development Environment">
    Set up your local development environment with the required libraries (jose, axios, etc.)
  </Step>
</Steps>

## Testing Workflow

Testing Dynamic QR Ph requires simulating the entire payment flow:

<Steps>
  <Step title="Register Webhook">
    Create a webhook endpoint to receive transaction status updates
  </Step>

  <Step title="Create QR Code">
    Generate a Dynamic QR Ph using the Create API
  </Step>

  <Step title="Convert QR Body">
    Convert the Base64-encoded QR body to an image
  </Step>

  <Step title="Decode QR Image">
    Extract the raw QR value from the generated image
  </Step>

  <Step title="Simulate Payment">
    Use the Simulate Webhook API to trigger a payment event
  </Step>

  <Step title="Receive Webhook">
    Your webhook endpoint receives the transaction status
  </Step>
</Steps>

## Step 1: Register a Webhook

First, register your webhook URL to receive payment notifications:

```bash theme={null}
curl -X POST https://qrph.sbx.moduluslabs.io/v1/webhooks \
  -u sk_YOUR_SECRET_KEY: \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://your-app.com/webhooks/qr-payment",
    "events": ["payment.succeeded", "payment.failed"]
  }'
```

<Tip>
  Use services like [webhook.site](https://webhook.site) or [ngrok](https://ngrok.com) to create temporary webhook endpoints for testing.
</Tip>

## Step 2: Create a Dynamic QR Code

Generate a QR code for testing:

<CodeGroup>
  ```javascript Node.js theme={null}
  const axios = require('axios');
  const jose = require('jose');

  const SECRET_KEY = 'sk_YOUR_SECRET_KEY';
  const ENCRYPTION_KEY = 'YOUR_ENCRYPTION_KEY';

  async function createQR() {
    // Encrypt payload
    const payload = {
      activationCode: 'A9X4-B7P2-Q6Z8-M3L5',
      currency: 'PHP',
      amount: '500.00',
      merchantReferenceNumber: crypto.randomUUID()
    };

    const key = Buffer.from(ENCRYPTION_KEY, 'base64');
    const jweToken = await new jose.CompactEncrypt(
      new TextEncoder().encode(JSON.stringify(payload))
    )
      .setProtectedHeader({ alg: 'A256KW', enc: 'A256CBC-HS512' })
      .encrypt(key);

    // Make API request
    const response = await axios.post(
      'https://qrph.sbx.moduluslabs.io/v1/pay/qr',
      { Token: jweToken },
      {
        auth: { username: SECRET_KEY, password: '' },
        headers: { 'Content-Type': 'application/json' }
      }
    );

    // Decrypt response
    const { plaintext } = await jose.compactDecrypt(response.data.Token, key);
    const result = JSON.parse(new TextDecoder().decode(plaintext));

    console.log('Transaction ID:', result.id);
    console.log('QR Body (Base64):', result.qrBody);

    return result;
  }

  createQR();
  ```

  ```bash cURL theme={null}
  # First, encrypt your payload (use a script or library)
  # Then make the request:

  curl -X POST https://qrph.sbx.moduluslabs.io/v1/pay/qr \
    -u sk_YOUR_SECRET_KEY: \
    -H "Content-Type: application/json" \
    -d '{
      "Token": "eyJhbGciOiJBMjU2S1ciL..."
    }'
  ```

  ```python Python theme={null}
  import os
  import json
  import requests
  from jose import jwe

  SECRET_KEY = os.getenv('MODULUS_SECRET_KEY')
  ENCRYPTION_KEY = os.getenv('MODULUS_ENCRYPTION_KEY')

  def create_qr():
      payload = {
          'activationCode': 'A9X4-B7P2-Q6Z8-M3L5',
          'currency': 'PHP',
          'amount': '500.00',
          'merchantReferenceNumber': f'TEST-{int(__import__("time").time())}'
      }

      jwe_token = jwe.encrypt(
          json.dumps(payload),
          ENCRYPTION_KEY,
          algorithm='A256KW',
          encryption='A256CBC-HS512'
      )

      response = requests.post(
          'https://qrph.sbx.moduluslabs.io/v1/pay/qr',
          json={'Token': jwe_token},
          auth=(SECRET_KEY, ''),
          headers={'Content-Type': 'application/json'}
      )

      decrypted = jwe.decrypt(response.json()['Token'], ENCRYPTION_KEY)
      result = json.loads(decrypted)

      print(f'Transaction ID: {result["id"]}')
      return result

  create_qr()
  ```

  ```php PHP theme={null}
  <?php
  require 'vendor/autoload.php';

  use Jose\Component\Core\AlgorithmManager;
  use Jose\Component\Core\JWK;
  use Jose\Component\Encryption\Algorithm\KeyEncryption\A256KW;
  use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;
  use Jose\Component\Encryption\Compression\CompressionMethodManager;
  use Jose\Component\Encryption\JWEBuilder;
  use Jose\Component\Encryption\Serializer\CompactSerializer;

  $secretKey = getenv('MODULUS_SECRET_KEY');
  $encryptionKey = getenv('MODULUS_ENCRYPTION_KEY');

  $jwk = new JWK(['kty' => 'oct', 'k' => $encryptionKey]);

  $payload = json_encode([
      'activationCode' => 'A9X4-B7P2-Q6Z8-M3L5',
      'currency' => 'PHP',
      'amount' => '500.00',
      'merchantReferenceNumber' => uniqid('TEST-', true)
  ]);

  $jweBuilder = new JWEBuilder(
      new AlgorithmManager([new A256KW()]),
      new AlgorithmManager([new A256CBCHS512()]),
      new CompressionMethodManager()
  );

  $jwe = $jweBuilder->create()
      ->withPayload($payload)
      ->withSharedProtectedHeader(['alg' => 'A256KW', 'enc' => 'A256CBC-HS512'])
      ->addRecipient($jwk)
      ->build();

  $token = (new CompactSerializer())->serialize($jwe, 0);

  $ch = curl_init('https://qrph.sbx.moduluslabs.io/v1/pay/qr');
  curl_setopt($ch, CURLOPT_POST, 1);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['Token' => $token]));
  curl_setopt($ch, CURLOPT_USERPWD, $secretKey . ':');
  curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

  $response = json_decode(curl_exec($ch), true);
  curl_close($ch);

  echo "Response received\n";
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"encoding/base64"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"os"

  	"github.com/lestrrat-go/jwx/v2/jwa"
  	"github.com/lestrrat-go/jwx/v2/jwe"
  )

  func main() {
  	secretKey := os.Getenv("MODULUS_SECRET_KEY")
  	encryptionKey := os.Getenv("MODULUS_ENCRYPTION_KEY")

  	payload, _ := json.Marshal(map[string]string{
  		"activationCode":          "A9X4-B7P2-Q6Z8-M3L5",
  		"currency":                "PHP",
  		"amount":                  "500.00",
  		"merchantReferenceNumber": "TEST-001",
  	})

  	keyBytes, _ := base64.RawURLEncoding.DecodeString(encryptionKey)
  	encrypted, _ := jwe.Encrypt(payload,
  		jwe.WithKey(jwa.A256KW, keyBytes),
  		jwe.WithContentEncryption(jwa.A256CBC_HS512),
  	)

  	reqBody, _ := json.Marshal(map[string]string{"Token": string(encrypted)})
  	req, _ := http.NewRequest("POST", "https://qrph.sbx.moduluslabs.io/v1/pay/qr", bytes.NewBuffer(reqBody))
  	req.SetBasicAuth(secretKey, "")
  	req.Header.Set("Content-Type", "application/json")

  	resp, _ := (&http.Client{}).Do(req)
  	defer resp.Body.Close()
  	body, _ := io.ReadAll(resp.Body)

  	var tokenResp map[string]string
  	json.Unmarshal(body, &tokenResp)

  	decrypted, _ := jwe.Decrypt([]byte(tokenResp["Token"]), jwe.WithKey(jwa.A256KW, keyBytes))

  	var result map[string]string
  	json.Unmarshal(decrypted, &result)
  	fmt.Printf("Transaction ID: %s\n", result["id"])
  }
  ```

  ```java Java theme={null}
  import com.nimbusds.jose.*;
  import com.nimbusds.jose.crypto.*;
  import com.nimbusds.jose.jwk.OctetSequenceKey;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import java.net.URI;
  import java.net.http.*;
  import java.util.*;

  public class CreateQRTest {
      public static void main(String[] args) throws Exception {
          String secretKey = System.getenv("MODULUS_SECRET_KEY");
          String encryptionKey = System.getenv("MODULUS_ENCRYPTION_KEY");
          ObjectMapper mapper = new ObjectMapper();

          Map<String, String> payload = Map.of(
              "activationCode", "A9X4-B7P2-Q6Z8-M3L5",
              "currency", "PHP",
              "amount", "500.00",
              "merchantReferenceNumber", "TEST-" + System.currentTimeMillis()
          );

          byte[] keyBytes = Base64.getUrlDecoder().decode(encryptionKey);
          OctetSequenceKey jwk = new OctetSequenceKey.Builder(keyBytes).build();

          JWEObject jwe = new JWEObject(
              new JWEHeader(JWEAlgorithm.A256KW, EncryptionMethod.A256CBC_HS512),
              new Payload(mapper.writeValueAsString(payload))
          );
          jwe.encrypt(new AESEncrypter(jwk));

          String auth = Base64.getEncoder().encodeToString((secretKey + ":").getBytes());
          HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create("https://qrph.sbx.moduluslabs.io/v1/pay/qr"))
              .header("Authorization", "Basic " + auth)
              .header("Content-Type", "application/json")
              .POST(HttpRequest.BodyPublishers.ofString(
                  mapper.writeValueAsString(Map.of("Token", jwe.serialize()))
              ))
              .build();

          HttpResponse<String> response = HttpClient.newHttpClient()
              .send(request, HttpResponse.BodyHandlers.ofString());

          Map<String, String> respData = mapper.readValue(response.body(), Map.class);
          JWEObject respJwe = JWEObject.parse(respData.get("Token"));
          respJwe.decrypt(new AESDecrypter(jwk));

          Map<String, String> result = mapper.readValue(respJwe.getPayload().toString(), Map.class);
          System.out.println("Transaction ID: " + result.get("id"));
      }
  }
  ```

  ```csharp C# / .NET theme={null}
  using System.Net.Http.Headers;
  using System.Text;
  using System.Text.Json;
  using Jose;

  var secretKey = Environment.GetEnvironmentVariable("MODULUS_SECRET_KEY");
  var encryptionKey = Environment.GetEnvironmentVariable("MODULUS_ENCRYPTION_KEY");

  static byte[] Base64UrlDecode(string base64Url)
  {
      var base64 = base64Url.Replace('-', '+').Replace('_', '/');
      switch (base64.Length % 4)
      {
          case 2: base64 += "=="; break;
          case 3: base64 += "="; break;
      }
      return Convert.FromBase64String(base64);
  }

  var key = Base64UrlDecode(encryptionKey);

  var payload = new
  {
      activationCode = "A9X4-B7P2-Q6Z8-M3L5",
      currency = "PHP",
      amount = "500.00",
      merchantReferenceNumber = $"TEST-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"
  };

  var jweToken = JWT.Encode(
      JsonSerializer.Serialize(payload), key,
      JweAlgorithm.A256KW, JweEncryption.A256CBC_HS512
  );

  using var client = new HttpClient();
  client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
      "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{secretKey}:"))
  );

  var response = await client.PostAsync(
      "https://qrph.sbx.moduluslabs.io/v1/pay/qr",
      new StringContent(
          JsonSerializer.Serialize(new { Token = jweToken }),
          Encoding.UTF8, "application/json"
      )
  );

  var responseBody = await response.Content.ReadAsStringAsync();
  var responseData = JsonSerializer.Deserialize<JsonElement>(responseBody);
  var decryptedJson = JWT.Decode(responseData.GetProperty("Token").GetString(), key);
  var result = JsonSerializer.Deserialize<JsonElement>(decryptedJson);

  Console.WriteLine($"Transaction ID: {result.GetProperty("id")}");
  ```
</CodeGroup>

**Response (encrypted JWE token):**

```json theme={null}
{
  "Token": "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIn0..."
}
```

**Decrypted payload:**

```json theme={null}
{
  "id": "0ce32626-08cf-405c-adab-6d384a8871da",
  "qrBody": "iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAAMTElEQVR4nO..."
}
```

## Step 3: Convert Base64 to QR Image

The `qrBody` field contains a Base64-encoded PNG image. Convert it to an image file:

<CodeGroup>
  ```javascript Node.js theme={null}
  const fs = require('fs');

  function saveQRImage(base64Data, filename = 'qr-code.png') {
    const buffer = Buffer.from(base64Data, 'base64');
    fs.writeFileSync(filename, buffer);
    console.log(`QR code saved to ${filename}`);
  }

  // Usage
  const result = await createQR();
  saveQRImage(result.qrBody, 'test-qr.png');
  ```

  ```python Python theme={null}
  import base64

  def save_qr_image(base64_data, filename='qr-code.png'):
      image_data = base64.b64decode(base64_data)
      with open(filename, 'wb') as f:
          f.write(image_data)
      print(f'QR code saved to {filename}')

  # Usage
  result = create_qr()
  save_qr_image(result['qrBody'], 'test-qr.png')
  ```

  ```bash Command Line theme={null}
  # Save base64 data to a file first, then:
  base64 -d qr-body.txt > qr-code.png
  ```
</CodeGroup>

### Online Converter (For Quick Testing)

Alternatively, use this online tool:

1. Visit [https://base64.guru/converter/decode/image/png](https://base64.guru/converter/decode/image/png)
2. Paste your Base64-encoded QR body
3. Click "Decode" to generate the image
4. Download the PNG file

**Example QR Code:**

<Frame>
  <img src="https://mintcdn.com/moduluslabsinc/ZYDbN28N7e564nyb/images/example-qr-code.jpeg?fit=max&auto=format&n=ZYDbN28N7e564nyb&q=85&s=143374bef24fd67f2f8ab397d0125a59" alt="Example QR Ph Code" width="179" height="172" data-path="images/example-qr-code.jpeg" />
</Frame>

## Step 4: Decode the QR Image

Extract the raw QR value from the image:

### Using Online Tool

1. Visit [https://zxing.org/w/decode.jspx](https://zxing.org/w/decode.jspx)
2. Upload your QR code image
3. Click "Submit"
4. Copy the raw text value

**Example raw QR value:**

```
00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX03258eff02de-e172-4b0d-bc5b-3041288500100000905033015204601653036085407 1500.005802PH5912MAIN ACCOUNT6006MANILA63041943
```

### Using Code

<CodeGroup>
  ```javascript Node.js (using jsqr) theme={null}
  const fs = require('fs');
  const { PNG } = require('pngjs');
  const jsQR = require('jsqr');

  function decodeQR(filename) {
    const data = fs.readFileSync(filename);
    const png = PNG.sync.read(data);

    const code = jsQR(
      Uint8ClampedArray.from(png.data),
      png.width,
      png.height
    );

    if (code) {
      console.log('QR Value:', code.data);
      return code.data;
    } else {
      throw new Error('Could not decode QR code');
    }
  }

  // Usage
  const qrValue = decodeQR('test-qr.png');
  ```

  ```python Python (using pyzbar) theme={null}
  from PIL import Image
  from pyzbar.pyzbar import decode

  def decode_qr(filename):
      img = Image.open(filename)
      decoded = decode(img)

      if decoded:
          qr_value = decoded[0].data.decode('utf-8')
          print('QR Value:', qr_value)
          return qr_value
      else:
          raise ValueError('Could not decode QR code')

  # Usage
  qr_value = decode_qr('test-qr.png')
  ```
</CodeGroup>

## Step 5: Simulate Payment

Now simulate a payment by calling the Simulate Webhook API:

<CodeGroup>
  ```javascript Node.js theme={null}
  const axios = require('axios');

  async function simulatePayment(qrValue, useCase = 'SUCCESS') {
    const response = await axios.post(
      'https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate',
      {
        qrBody: qrValue,
        useCase: useCase  // 'SUCCESS' or error code
      },
      {
        auth: { username: SECRET_KEY, password: '' },
        headers: { 'Content-Type': 'application/json' }
      }
    );

    console.log('Simulation triggered:', response.data);
    return response.data;
  }

  // Usage - Successful payment
  await simulatePayment(qrValue, 'SUCCESS');

  // Usage - Failed payment
  await simulatePayment(qrValue, 'MISSING_PARTNER_REVENUE_ACCOUNT');
  ```

  ```bash cURL theme={null}
  # Successful payment
  curl -X POST https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate \
    -u sk_YOUR_SECRET_KEY: \
    -H "Content-Type: application/json" \
    -d '{
      "qrBody": "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX...",
      "useCase": "SUCCESS"
    }'

  # Failed payment
  curl -X POST https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate \
    -u sk_YOUR_SECRET_KEY: \
    -H "Content-Type: application/json" \
    -d '{
      "qrBody": "00020101021228820011ph.ppmi.p2m0111CUOBPHM2XXX...",
      "useCase": "MISSING_PARTNER_REVENUE_ACCOUNT"
    }'
  ```

  ```python Python theme={null}
  import requests

  SECRET_KEY = 'sk_YOUR_SECRET_KEY'

  def simulate_payment(qr_value, use_case='SUCCESS'):
      response = requests.post(
          'https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate',
          json={'qrBody': qr_value, 'useCase': use_case},
          auth=(SECRET_KEY, ''),
          headers={'Content-Type': 'application/json'}
      )

      print(f'Simulation triggered: {response.json()}')
      return response.json()

  simulate_payment(qr_value, 'SUCCESS')
  ```

  ```php PHP theme={null}
  <?php
  $secretKey = 'sk_YOUR_SECRET_KEY';

  $ch = curl_init('https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate');
  curl_setopt($ch, CURLOPT_POST, 1);
  curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
      'qrBody' => $qrValue,
      'useCase' => 'SUCCESS'
  ]));
  curl_setopt($ch, CURLOPT_USERPWD, $secretKey . ':');
  curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

  $response = curl_exec($ch);
  curl_close($ch);

  echo "Simulation triggered: $response\n";
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  )

  func simulatePayment(secretKey, qrValue, useCase string) {
  	body, _ := json.Marshal(map[string]string{
  		"qrBody":  qrValue,
  		"useCase": useCase,
  	})

  	req, _ := http.NewRequest("POST",
  		"https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate",
  		bytes.NewBuffer(body))
  	req.SetBasicAuth(secretKey, "")
  	req.Header.Set("Content-Type", "application/json")

  	resp, _ := (&http.Client{}).Do(req)
  	defer resp.Body.Close()
  	result, _ := io.ReadAll(resp.Body)

  	fmt.Printf("Simulation triggered: %s\n", string(result))
  }
  ```

  ```java Java theme={null}
  import java.net.URI;
  import java.net.http.*;
  import java.util.Base64;

  public class SimulatePayment {
      public static void main(String[] args) throws Exception {
          String secretKey = "sk_YOUR_SECRET_KEY";
          String auth = Base64.getEncoder().encodeToString((secretKey + ":").getBytes());

          String body = String.format(
              "{\"qrBody\":\"%s\",\"useCase\":\"SUCCESS\"}", qrValue
          );

          HttpRequest request = HttpRequest.newBuilder()
              .uri(URI.create("https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate"))
              .header("Authorization", "Basic " + auth)
              .header("Content-Type", "application/json")
              .POST(HttpRequest.BodyPublishers.ofString(body))
              .build();

          HttpResponse<String> response = HttpClient.newHttpClient()
              .send(request, HttpResponse.BodyHandlers.ofString());

          System.out.println("Simulation triggered: " + response.body());
      }
  }
  ```

  ```csharp C# / .NET theme={null}
  using System.Net.Http.Headers;
  using System.Text;
  using System.Text.Json;

  var secretKey = "sk_YOUR_SECRET_KEY";

  using var client = new HttpClient();
  client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
      "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{secretKey}:"))
  );

  var response = await client.PostAsync(
      "https://qrph.sbx.moduluslabs.io/v1/webhooks/simulate",
      new StringContent(
          JsonSerializer.Serialize(new { qrBody = qrValue, useCase = "SUCCESS" }),
          Encoding.UTF8, "application/json"
      )
  );

  var result = await response.Content.ReadAsStringAsync();
  Console.WriteLine($"Simulation triggered: {result}");
  ```
</CodeGroup>

## Step 6: Receive Webhook Notification

After simulating the payment, your webhook endpoint will receive a notification:

### Successful Payment Webhook

```json theme={null}
{
  "event": "payment.succeeded",
  "transactionId": "0ce32626-08cf-405c-adab-6d384a8871da",
  "merchantReferenceNumber": "5e91bc6c-f7e2-4a39-ae0e-5e93985c94a4",
  "amount": "500.00",
  "currency": "PHP",
  "status": "succeeded",
  "completedAt": "2025-01-29T10:30:00Z"
}
```

### Failed Payment Webhook

```json theme={null}
{
  "event": "payment.failed",
  "transactionId": "0ce32626-08cf-405c-adab-6d384a8871da",
  "merchantReferenceNumber": "5e91bc6c-f7e2-4a39-ae0e-5e93985c94a4",
  "amount": "500.00",
  "currency": "PHP",
  "status": "failed",
  "errorCode": "MISSING_PARTNER_REVENUE_ACCOUNT",
  "errorMessage": "Partner revenue account not found",
  "failedAt": "2025-01-29T10:30:00Z"
}
```

## Test Scenarios

Test different scenarios to ensure your integration handles all cases:

<AccordionGroup>
  <Accordion title="Successful Payment" icon="circle-check">
    **Use Case:** `SUCCESS`

    **Expected Result:** Webhook receives `payment.succeeded` event

    **Test:** Verify your app processes successful payments correctly
  </Accordion>

  <Accordion title="Missing Revenue Account" icon="circle-xmark">
    **Use Case:** `MISSING_PARTNER_REVENUE_ACCOUNT`

    **Expected Result:** Webhook receives `payment.failed` event

    **Test:** Verify your app handles this specific error gracefully
  </Accordion>

  <Accordion title="Insufficient Funds" icon="wallet">
    **Use Case:** `INSUFFICIENT_FUNDS`

    **Expected Result:** Webhook receives `payment.failed` event

    **Test:** Ensure user is informed of insufficient funds
  </Accordion>

  <Accordion title="Invalid QR Code" icon="ban">
    **Use Case:** Use a malformed or expired QR value

    **Expected Result:** API returns error before webhook is sent

    **Test:** Verify error handling for invalid QR codes
  </Accordion>

  <Accordion title="Duplicate Payment" icon="copy">
    **Use Case:** Reuse the same `merchantReferenceNumber`

    **Expected Result:** API rejects duplicate transaction

    **Test:** Ensure idempotency is working correctly
  </Accordion>

  <Accordion title="Timeout Scenario" icon="clock">
    **Use Case:** Delay webhook simulation by 30+ seconds

    **Expected Result:** Test timeout handling in your application

    **Test:** Verify your app doesn't hang waiting for webhook
  </Accordion>
</AccordionGroup>

## Complete Test Script

Here's a complete end-to-end test script:

<CodeGroup>
  ```javascript Node.js theme={null}
  const axios = require('axios');
  const jose = require('jose');
  const fs = require('fs');

  const SECRET_KEY = process.env.MODULUS_SECRET_KEY;
  const ENCRYPTION_KEY = process.env.MODULUS_ENCRYPTION_KEY;

  async function runFullTest() {
    console.log('🧪 Starting QR Ph Integration Test\n');

    try {
      // Step 1: Create QR Code
      console.log('📝 Step 1: Creating Dynamic QR Code...');
      const payload = {
        activationCode: 'A9X4-B7P2-Q6Z8-M3L5',
        currency: 'PHP',
        amount: '500.00',
        merchantReferenceNumber: `TEST-${Date.now()}`
      };

      const key = Buffer.from(ENCRYPTION_KEY, 'base64');
      const jweToken = await new jose.CompactEncrypt(
        new TextEncoder().encode(JSON.stringify(payload))
      )
        .setProtectedHeader({ alg: 'A256KW', enc: 'A256CBC-HS512' })
        .encrypt(key);

      const createResponse = await axios.post(
        'https://qrph.sbx.moduluslabs.io/v1/pay/qr',
        { Token: jweToken },
        {
          auth: { username: SECRET_KEY, password: '' },
          headers: { 'Content-Type': 'application/json' }
        }
      );

      const { plaintext } = await jose.compactDecrypt(createResponse.data.Token, key);
      const qrData = JSON.parse(new TextDecoder().decode(plaintext));
      console.log(` QR Created - ID: ${qrData.id}\n`);

      // Step 2: Save QR Image
      console.log(' Step 2: Saving QR Image...');
      const buffer = Buffer.from(qrData.qrBody, 'base64');
      fs.writeFileSync('test-qr.png', buffer);
      console.log(' QR saved to test-qr.png\n');

      // Step 3: Decode QR (manual step - using placeholder)
      console.log(' Step 3: Decode QR manually using:');
      console.log('   https://zxing.org/w/decode.jspx');
      console.log('   Upload: test-qr.png\n');

      // Step 4: Simulate Payment (after you get qrValue from decoding)
      console.log(' Step 4: Ready to simulate payment');
      console.log('   Run: simulatePayment(qrValue, "SUCCESS")');
      console.log('\n Test setup complete!');

      return qrData;

    } catch (error) {
      console.error(' Test failed:', error.message);
      if (error.response) {
        console.error('Response:', error.response.data);
      }
      throw error;
    }
  }

  // Run the test
  runFullTest();
  ```

  ```python Python theme={null}
  import os
  import base64
  import json
  import time
  import requests
  from jose import jwe

  SECRET_KEY = os.getenv('MODULUS_SECRET_KEY')
  ENCRYPTION_KEY = os.getenv('MODULUS_ENCRYPTION_KEY')

  def run_full_test():
      print('🧪 Starting QR Ph Integration Test\n')

      try:
          # Step 1: Create QR Code
          print('📝 Step 1: Creating Dynamic QR Code...')
          payload = {
              'activationCode': 'A9X4-B7P2-Q6Z8-M3L5',
              'currency': 'PHP',
              'amount': '500.00',
              'merchantReferenceNumber': f'TEST-{int(time.time())}'
          }

          jwe_token = jwe.encrypt(
              json.dumps(payload),
              ENCRYPTION_KEY,
              algorithm='A256KW',
              encryption='A256CBC-HS512'
          )

          response = requests.post(
              'https://qrph.sbx.moduluslabs.io/v1/pay/qr',
              json={'Token': jwe_token},
              auth=(SECRET_KEY, ''),
              headers={'Content-Type': 'application/json'}
          )

          decrypted = jwe.decrypt(response.json()['Token'], ENCRYPTION_KEY)
          qr_data = json.loads(decrypted)
          print(f" QR Created - ID: {qr_data['id']}\n")

          # Step 2: Save QR Image
          print(' Step 2: Saving QR Image...')
          image_data = base64.b64decode(qr_data['qrBody'])
          with open('test-qr.png', 'wb') as f:
              f.write(image_data)
          print(' QR saved to test-qr.png\n')

          # Step 3: Instructions
          print(' Step 3: Decode QR manually using:')
          print('   https://zxing.org/w/decode.jspx')
          print('   Upload: test-qr.png\n')

          print('⏳ Step 4: Ready to simulate payment')
          print('   Run: simulate_payment(qr_value, "SUCCESS")')
          print('\n Test setup complete!')

          return qr_data

      except Exception as e:
          print(f' Test failed: {str(e)}')
          raise

  if __name__ == '__main__':
      run_full_test()
  ```
</CodeGroup>

## Debugging Tips

<CardGroup cols={2}>
  <Card title="Enable Verbose Logging" icon="terminal">
    Log all requests, responses, and encryption/decryption steps
  </Card>

  <Card title="Use Webhook.site" icon="webhook">
    Test webhook delivery without setting up a server
  </Card>

  <Card title="Check Base64 Encoding" icon="code">
    Ensure Base64 strings aren't corrupted during copy/paste
  </Card>

  <Card title="Verify Credentials" icon="key">
    Double-check you're using sandbox credentials, not production
  </Card>

  <Card title="Test Incrementally" icon="list-check">
    Test each step independently before running end-to-end
  </Card>

  <Card title="Monitor Webhook Logs" icon="eye">
    Check webhook server logs for incoming requests
  </Card>
</CardGroup>

## Common Issues

<AccordionGroup>
  <Accordion title="QR Code Won't Decode">
    * Ensure the image was saved correctly from Base64
    * Try a different QR decoder tool
    * Check if the image file is corrupted
    * Verify the Base64 string wasn't truncated
  </Accordion>

  <Accordion title="Webhook Not Received">
    * Verify webhook URL is publicly accessible
    * Check webhook registration was successful
    * Ensure firewall isn't blocking incoming requests
    * Use webhook.site for initial testing
  </Accordion>

  <Accordion title="Simulation Returns Error">
    * Verify the QR value was decoded correctly
    * Check that the transaction hasn't been used already
    * Ensure proper authentication
    * Verify the useCase parameter is valid
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Quickstart Guide" icon="rocket" href="/docs/qr/quickstart">
    Complete integration guide with code examples
  </Card>

  <Card title="Create QR API" icon="qrcode" href="/api-reference/qr/create">
    Full API reference for QR generation
  </Card>

  <Card title="Webhooks Documentation" icon="webhook" href="/docs/webhooks">
    Learn about webhook events and security
  </Card>

  <Card title="Go to Production" icon="rocket-launch" href="/docs/production">
    Steps to deploy your integration to production
  </Card>
</CardGroup>
