{"id":30893230,"url":"https://github.com/tchasinga/cylind-callback-url","last_synced_at":"2025-09-08T20:06:19.092Z","repository":{"id":301689050,"uuid":"1010026735","full_name":"tchasinga/cylind-callback-url","owner":"tchasinga","description":null,"archived":false,"fork":false,"pushed_at":"2025-07-08T04:31:54.000Z","size":52,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-07-08T06:03:08.571Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://cylind-callback-url.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tchasinga.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-28T07:24:46.000Z","updated_at":"2025-07-08T04:31:57.000Z","dependencies_parsed_at":"2025-06-28T08:40:41.555Z","dependency_job_id":null,"html_url":"https://github.com/tchasinga/cylind-callback-url","commit_stats":null,"previous_names":["tchasinga/cylind-callback-url"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tchasinga/cylind-callback-url","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchasinga%2Fcylind-callback-url","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchasinga%2Fcylind-callback-url/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchasinga%2Fcylind-callback-url/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchasinga%2Fcylind-callback-url/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tchasinga","download_url":"https://codeload.github.com/tchasinga/cylind-callback-url/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tchasinga%2Fcylind-callback-url/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274231512,"owners_count":25245600,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-08T02:00:09.813Z","response_time":121,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-08T20:06:16.125Z","updated_at":"2025-09-08T20:06:19.073Z","avatar_url":"https://github.com/tchasinga.png","language":"TypeScript","readme":"import { getPostgresPoolConnection } from '@/lib/mysqlconnection'\nimport { NextResponse } from 'next/server'\n\ninterface MpesaCallbackBody {\n  Body: {\n    stkCallback: {\n      MerchantRequestID: string\n      CheckoutRequestID: string\n      ResultCode: number\n      ResultDesc: string\n      CallbackMetadata?: {\n        Item: Array\u003c{\n          Name: string\n          Value: string | number\n        }\u003e\n      }\n    }\n  }\n}\n\nexport async function POST (request: Request) {\n  const client = await getPostgresPoolConnection().connect()\n  try {\n    const callbackData: MpesaCallbackBody = await request.json()\n\n    if (!callbackData?.Body?.stkCallback) {\n      console.error('Invalid callback structure:', callbackData)\n      return NextResponse.json(\n        { error: 'Invalid callback structure' },\n        { status: 400 }\n      )\n    }\n\n    const {\n      MerchantRequestID,\n      CheckoutRequestID,\n      ResultCode,\n      ResultDesc,\n      CallbackMetadata\n    } = callbackData.Body.stkCallback\n\n    console.log('M-Pesa Callback Received:', {\n      MerchantRequestID,\n      CheckoutRequestID,\n      ResultCode,\n      ResultDesc,\n      CallbackMetadata\n    })\n\n    const isSuccess = ResultCode === 0\n    let mpesaReceiptNumber = ''\n    let phoneNumber = ''\n    let amount = 0\n    let transactionDate = ''\n\n    if (isSuccess \u0026\u0026 CallbackMetadata) {\n      for (const item of CallbackMetadata.Item) {\n        switch (item.Name) {\n          case 'MpesaReceiptNumber':\n            mpesaReceiptNumber = String(item.Value)\n            break\n          case 'PhoneNumber':\n            phoneNumber = String(item.Value)\n            break\n          case 'Amount':\n            amount = Number(item.Value)\n            break\n          case 'TransactionDate':\n            transactionDate = String(item.Value)\n            break\n        }\n      }\n    }\n\n    // Improved payment matching logic\n    const formattedPhone = phoneNumber ? `254${phoneNumber.slice(-9)}` : null\n\n    // Try matching by CheckoutRequestID first (most reliable)\n    let paymentId: number | null = null\n    let matchMethod = ''\n\n    // 1. Try to match by CheckoutRequestID if we have it\n    if (CheckoutRequestID) {\n      const result = await client.query(\n        `SELECT id FROM achievepayemetwithmpesa \n         WHERE checkout_request_id = $1 LIMIT 1`,\n        [CheckoutRequestID]\n      )\n      if (result.rows.length \u003e 0) {\n        paymentId = result.rows[0].id\n        matchMethod = 'CheckoutRequestID'\n      }\n    }\n\n    // 2. If not found, try matching by phone and amount (within last 30 minutes)\n    if (!paymentId \u0026\u0026 formattedPhone \u0026\u0026 amount) {\n      const result = await client.query(\n        `SELECT id FROM achievepayemetwithmpesa \n         WHERE mpesa_number = $1 \n         AND totalcost = $2\n         AND (payment_status IS NULL OR payment_status = 'pending')\n         AND created_at \u003e= NOW() - INTERVAL '30 minutes'\n         ORDER BY created_at DESC LIMIT 1`,\n        [formattedPhone, amount]\n      )\n      if (result.rows.length \u003e 0) {\n        paymentId = result.rows[0].id\n        matchMethod = 'PhoneAndAmount'\n      }\n    }\n\n    if (!paymentId) {\n      console.error('No matching payment found for:', {\n        CheckoutRequestID,\n        formattedPhone,\n        amount,\n        transactionDate\n      })\n\n      // Create a new record if we have complete payment info but no match\n      if (isSuccess \u0026\u0026 formattedPhone \u0026\u0026 amount \u0026\u0026 mpesaReceiptNumber) {\n        const insertResult = await client.query(\n          `INSERT INTO achievepayemetwithmpesa \n           (resellername, totalcost, mpesa_number, payment_status,\n            mpesa_receipt_number, transaction_date, result_code,\n            result_description, merchant_request_id, checkout_request_id)\n           VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n           RETURNING id`,\n          [\n            resellername, // resellername is not available, set to null\n            amount,\n            formattedPhone,\n            'completed',\n            mpesaReceiptNumber,\n            transactionDate,\n            ResultCode,\n            ResultDesc,\n            MerchantRequestID,\n            CheckoutRequestID\n          ]\n        )\n        paymentId = insertResult.rows[0].id\n        matchMethod = 'NewRecordCreated'\n        console.log(`Created new payment record ${paymentId} from callback`)\n      } else {\n        return NextResponse.json({\n          ResultCode: 1,\n          ResultDesc:\n            'No matching payment found and insufficient data to create new record'\n        })\n      }\n    }\n\n    // Update payment record if we found or created one\n    if (paymentId) {\n      await client.query(\n        `UPDATE achievepayemetwithmpesa \n         SET \n           payment_status = $1,\n           mpesa_receipt_number = COALESCE($2, mpesa_receipt_number),\n           transaction_date = COALESCE($3, transaction_date),\n           result_code = $4,\n           result_description = $5,\n           merchant_request_id = COALESCE($6, merchant_request_id),\n           checkout_request_id = COALESCE($7, checkout_request_id)\n         WHERE id = $8`,\n        [\n          isSuccess ? 'completed' : 'failed',\n          mpesaReceiptNumber,\n          transactionDate,\n          ResultCode,\n          ResultDesc,\n          MerchantRequestID,\n          CheckoutRequestID,\n          paymentId\n        ]\n      )\n\n      console.log(`Payment ${paymentId} updated (matched by ${matchMethod})`)\n    }\n\n    return NextResponse.json({\n      ResultCode: 0,\n      ResultDesc: 'Callback processed successfully'\n    })\n  } catch (error) {\n    console.error('Callback processing error:', error)\n    return NextResponse.json(\n      {\n        ResultCode: 1,\n        ResultDesc: 'Error processing callback'\n      },\n      { status: 500 }\n    )\n  } finally {\n    client.release()\n  }\n}\n\n// get request\n\nexport async function GET () {\n  return NextResponse.json(\n    { message: 'This endpoint only accepts POST requests.' },\n    { status: 405 }\n  )\n}\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftchasinga%2Fcylind-callback-url","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftchasinga%2Fcylind-callback-url","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftchasinga%2Fcylind-callback-url/lists"}