付款完成后如何通过电子邮件发送发票
#node #api #payments #invoice

kevin kimani

发票管理是一个过程,涉及为客户购买的商品或服务生成,发送,跟踪和调解发票。发票管理对电子商务客户非常重要,因为它使他们能够跟踪其财务交易并确保正确收费。它还可以帮助客户保持井井有条,并能够更有效地管理自己的财务。

Rapyd Collect是由Fintech公司Rapyd提供的API,该公司提供了全球支付处理平台。 Rapyd Collect API允许来自世界各地的企业通过多种付款方式轻松地从其客户那里收取付款,包括银行转移,银行重定向,卡,现金和本地Ewallet。

使用Rapyd Collect API,您可以构建一个电子商务平台,使您可以在Rapyd上创建产品,通过Rapyd Checkout页面收集付款,为购买的产品创建发票,并集成了邮件服务以自动进行邮件服务付款完成后,将发票发送给客户。

在本教程中,您将学习使用Rapyd Collect API创建和发送自定义发票。为此,您将:

  • 创建一个包含产品页面的Node.js Web应用
  • 集成Rapyd Checkout Flow
  • 使用Rapyd Collect API在Rapyd中创建产品(服务),设置计划和订阅,并创建一个发票,其中包含客户端订阅的项目
  • 集成邮件服务(MailGun)以自动将发票发送给客户一旦付款完成

您可以在this GitHub repo中找到本教程的代码。

付款后实施自动发票电子邮件

要遵循上述系统,您需要:

  • node.js,npm,代码编辑器和本地计算机上安装的浏览器。
  • Rapyd帐户。
  • Postman在您的本地机器上设置。您也可以使用web version
  • 一个Mailgun帐户。 Sign up进行免费试用,并确保验证您的电子邮件。

您还需要前往Rapyd Docs并学习如何进行第一个API调用。

建立开发环境

要开始,创建一个称为rapyd-invoice的文件夹。在您的代码编辑器中打开它,然后在终端中运行以下命令:

npm init -y

此命令将在rapyd-invoice目录中初始化一个node.js项目,并创建一个package.json文件。接下来,运行下面的命令以安装该项目将使用的依赖项:

npm i express dotenv ejs node-persist nodemon body-parser mailgun-js

这些依赖项包括:

  • express-设置node.js服务器。
  • dotenv-加载环境变量。
  • ejs-用作模板引擎。
  • node-persist-在服务器上创建本地存储。
  • nodemon-监视服务器代码中的更改并自动重新启动。
  • body-parser-解析传入的请求机构。
  • mailgun-js-将发票邮寄给客户。

接下来,修改package.json scripts键值配对,以包括命令启动服务器的命令:

  "scripts": {
    "start": "nodemon index.js"
  },

然后在项目root文件夹中创建一个utils.js文件,并添加下面的代码,Rapyd提供了该文件,以帮助与其API互动:

require('dotenv').config({path: './.env'});

const https = require('https');
const crypto = require('crypto');
const accessKey = process.env.RAPYD_ACCESS_KEY
const secretKey = process.env.RAPYD_SECRET_KEY
const log = false;

async function makeRequest(method, urlPath, body = null) {

    try {
     httpMethod = method;
     httpBaseURL = "sandboxapi.rapyd.net";
     httpURLPath = urlPath;
     salt = generateRandomString(8);
     idempotency = new Date().getTime().toString();
     timestamp = Math.round(new Date().getTime() / 1000);
     signature = sign(httpMethod, httpURLPath, salt, timestamp, body)

     const options = {
         hostname: httpBaseURL,
         port: 443,
         path: httpURLPath,
         method: httpMethod,
         headers: {
             'Content-Type': 'application/json',
             salt: salt,
             timestamp: timestamp,
             signature: signature,
             access_key: accessKey,
             idempotency: idempotency
         }
     }

     return await httpRequest(options, body, log);
    }
    catch (error) {
     console.error("Error generating request options");
     throw error;
    }
}

function sign(method, urlPath, salt, timestamp, body) {

    try {
     let bodyString = "";
     if (body) {
         bodyString = JSON.stringify(body);
         bodyString = bodyString == "{}" ? "" : bodyString;
     }

     let toSign = method.toLowerCase() + urlPath + salt + timestamp + accessKey + secretKey + bodyString;
     log && console.log(`toSign: ${toSign}`);

     let hash = crypto.createHmac('sha256', secretKey);
     hash.update(toSign);
     const signature = Buffer.from(hash.digest("hex")).toString("base64")
     log && console.log(`signature: ${signature}`);

     return signature;
    }
    catch (error) {
     console.error("Error generating signature");
     throw error;
    }
}

function generateRandomString(size) {
    try {
     return crypto.randomBytes(size).toString('hex');
    }
    catch (error) {
     console.error("Error generating salt");
     throw error;
    }
}

async function httpRequest(options, body) {

    return new Promise((resolve, reject) => {

     try {

         let bodyString = "";
         if (body) {
             bodyString = JSON.stringify(body);
             bodyString = bodyString == "{}" ? "" : bodyString;
         }

         log && console.log(`httpRequest options: ${JSON.stringify(options)}`);
         const req = https.request(options, (res) => {
             let response = {
                 statusCode: res.statusCode,
                 headers: res.headers,
                 body: ''
             };

             res.on('data', (data) => {
                 response.body += data;
             });

             res.on('end', () => {

                 response.body = response.body ? JSON.parse(response.body) : {}
                 log && console.log(`httpRequest response: ${JSON.stringify(response)}`);

                 if (response.statusCode !== 200) {
                     return reject(response);
                 }

                 return resolve(response);
             });
         })

         req.on('error', (error) => {
             return reject(error);
         })

         req.write(bodyString)
         req.end();
     }
     catch(err) {
         return reject(err);
     }
    })

}

exports.makeRequest = makeRequest;

接下来,在项目root文件夹中创建一个.env文件并粘贴以下内容:

RAPYD_ACCESS_KEY=
RAPYD_SECRET_KEY=
MAILGUN_API_KEY=
MAILGUN_DOMAIN=
MAILGUN_SENDER_EMAIL=

要获取Rapyd访问和秘密键的值,请按照在获取API键下提供的here的说明。

要获取Mailgun字段的值,请登录您的Mailgun帐户,然后单击侧栏上的发送。选择,复制提供的域,然后将其分配给MAILGUN_DOMAIN

An image showing Mailgun domains

接下来,在右上角单击您的帐户名。在出现的下拉中,选择 api键。复制私有API键的值并将其分配给MAILGUN_API_KEY

An image showing Mailgun API keys

对于MAILGUN_SENDER_EMAIL,请使用您用来创建Mailgun帐户的电子邮件。

最后,在项目root文件夹中创建一个index.js文件,然后添加下面的代码以创建基本的Express Server:

// import the required dependencies
require('dotenv').config({path: './.env'});

const express = require('express')
var bodyParser = require('body-parser')
const storage = require('node-persist');

var mailgun = require('mailgun-js')({apiKey: process.env.MAILGUN_API_KEY, domain: process.env.MAILGUN_DOMAIN});

storage.init()

const app = express()

// set the port
const port = 3000

// import the utility file for making requests to the Rapyd API
const makeRequest = require('./utils').makeRequest;

// variables
const planId = "<your-plan-id>"

//set the render engine
app.set("view engine", "ejs")

// parse application/json
app.use(bodyParser.json())

//Add routes here

app.listen(port, () => {
    console.log(`Example app listening on port ${port}`)
  })

您可以通过在终端中运行以下命令来确认服务器正在成功运行:

npm start

创建产品页面

产品页面将显示服务的详细信息,以及客户将输入其详细信息的表格。在实施此页面之前,您需要在Rapyd中创建一个productplan

使用Rapyd,产品可以为goods型或service。对于本教程,您将创建一个service产品。要了解有关goods的更多信息,您可以查看official documentationservice产品需要附加到描述服务定价结构的计划。然后将计划附加到subscription

service类型的create a product,您将使用Rapyd Postman collection。在Web的Postman或您本地计算机上安装的Postman应用程序中打开该系列。

加载集合后,您应该有类似的东西:

An image showing the loaded postman collection

接下来,您需要创建一个包含Rapyd访问和秘密键的Rapyd环境,以及将发送API请求的base uri。单击环境在左侧栏中,创建一个包含所需字段的新环境。您应该以这样的方式结束:

An image showing the created Rapyd environment

然后返回收集标签,并在右上角加载新创建的环境:

An image showing the Postman collection with the loaded environment

接下来,从Rapyd API集合中搜索“创建服务”。从结果中,单击创建服务,然后在打开的选项卡中,发送带有以下请求的邮政请求:

{
    "id": "",
    "name": "Monthly Parking Service Booking",
    "type": "services",
    "active": true,
    "description": "Sign up for our monthly parking service and enjoy the convenience of having a guaranteed spot every day",
    "images": [
     "https://images.unsplash.com/photo-1506521781263-d8422e82f27a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
    ],
    "shippable": false,
    "statement_descriptor": "",
    "unit_label": ""
}

official documentation中讨论了有关这些字段的更多信息。

从收到的响应中,复制代表product_iddata.id,因为它将用于创建计划。

An image showing how to create a service

要创建一个计划,您将使用Postman。从Rapyd API集合中搜索“创建计划”。从搜索结果中,选择使用产品ID 选项创建计划。从打开的选项卡中,发送带有以下请求的邮政请求:

记住将your-product-id替换为您从上一步收到的相应产品ID。

{
    "currency": "USD",
    "interval": "month",
            “amount”: 1150,
    "product": "your-product-id",
    "aggregate_usage": "",
    "billing_scheme": "per_unit",
    "nickname": "basic",
    "transform_usage": {},
    "trial_period_days": 0,
    "usage_type": "licensed"
}

official documentation中讨论了有关这些字段的更多信息。

An image showing how to create a plan

从响应中复制计划ID,因为您将使用它来查询计划和产品,并在网页上显示数据。打开index.js文件并将您的计划ID分配给已经定义的const planId

现在,您可以创建一个查询产品并计划信息的路线,然后将其返回到前端。将以下代码添加到index.js文件:

确保在app.listen()函数之前添加代码。

// Route to query the plan and return plan+product details
app.get('/', async (req, res) => {
    try {
     const { body: { data } } = await makeRequest('GET', `/v1/plans/${planId}`);

     await storage.setItem('productPrice', data.amount)

     res.render('product', {
         title: data.product.name,
         price: data.amount,
         description: data.product.description,
         image: data.product.images[0]
     })
    } catch (error) {
     console.log(error)
    }
})

在上面的代码中,您:

  • 向Rapyd收集API的请求以检索计划信息。
  • 将产品价格保存到本地存储。
  • 提取所需的数据,并将其与在此路线请求时将渲染的模板一起返回客户端。

要创建product模板,请在项目root文件夹中创建一个称为views的文件夹。在其中,创建一个product.ejs文件并添加以下代码:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Product Page</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
    <style>
     .hidden{
         display: none;
     }
    </style>
</head>
  <body class="container-sm text-center">
    <!-- Display product -->
    <div class="mt-3 d-flex flex-row">
     <img src=<%=image%> alt="Product image" style="max-width: 450px;">

     <div class="px-5 py-3 text-start d-flex flex-column justify-content-between align-items-start">
         <h2 class="title"><%=title%></h2>
         <p class="description"><%=description%></p>
         <p class="price"><strong>Price:</strong> $<%=price%> (Billed monthly)</p>
         <button class="btn btn-primary" id="purchaseBtn">Book Now</button>
     </div>
    </div>

    <!-- Customer details form -->
    <div class="mt-5 hidden" id="custContainer">
     <hr>
     <h3>Customer Details</h3>
     <form class="text-start row g-3">
         <h5>Personal Details</h5>
         <div class="col-md-4">
             <label for="custName" class="form-label">Name</label>
             <input type="text" class="form-control" id="custName" value="Jane Doe">
         </div>
         <div class="col-md-4">
             <label for="custEmail" class="form-label">Email</label>
             <input type="text" class="form-control" id="custEmail">
         </div>
         <div class="col-md-4">
             <label for="custPhone" class="form-label">Phone</label>
             <input type="text" class="form-control" id="custPhone" value="+14155559993">
         </div>

         <h5>Payment Details</h5>
         <div class="col-3">
             <label for="custCardNo" class="form-label">Card Number</label>
             <input type="text" class="form-control" id="custCardNo" placeholder="4111 1111 1111 1111" value="4111111111111111">
         </div>
         <div class="col-3">
             <label for="custCardExpMonth" class="form-label">Expiry Month</label>
             <input type="number" class="form-control" id="custCardExpMonth" placeholder="12" value="12">
         </div>
         <div class="col-3">
             <label for="custCardExpYear" class="form-label">Expiry Year</label>
             <input type="number" class="form-control" id="custCardExpYear" placeholder="34" value="34">
         </div>
         <div class="col-3">
             <label for="custCardCVV" class="form-label">CVV</label>
             <input type="number" class="form-control" id="custCardCVV" placeholder="567" value="567">
         </div>

         <div class="col-12">
             <button type="submit" id="checkoutBtn" class="btn btn-primary">Checkout</button>
         </div>
         </form>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
    <script>
     const purchaseBtn = document.getElementById('purchaseBtn')
     const custContainer = document.getElementById('custContainer')
     const checkoutBtn = document.getElementById("checkoutBtn")

     purchaseBtn.addEventListener('click', () => {
         custContainer.classList.remove('hidden')
         purchaseBtn.setAttribute('disabled', 'true')
         purchaseBtn.innerText = 'Provide your details below'
     })

     custContainer.addEventListener('submit', (e) => {
         e.preventDefault()

         const customerDetails = {
         name: document.getElementById('custName').value,
         email: document.getElementById('custEmail').value,
         phone: document.getElementById('custPhone').value,
         cardNo: document.getElementById('custCardNo').value,
         cardExpMonth: document.getElementById('custCardExpMonth').value,
         cardExpYear: document.getElementById('custCardExpYear').value,
         cardCVV: document.getElementById('custCardCVV').value
     }

         console.log(customerDetails)

         fetch('http://localhost:3000/', {
             method: 'POST',
             headers: {
                 'Content-Type': 'application/json'
             },
             body: JSON.stringify({
                 customerDetails: customerDetails
             })
         }).then(res => {
           console.log(res)
           if(res.status === 200){
               window.location.assign('http://localhost:3000/checkout')
           }
     })

     })
    </script>
  </body>
</html>

上面的代码呈现一个显示产品信息的模板。它还具有一个名为 Book Now 的按钮,单击时,它会显示一张表格,以供客户填写其详细信息。该表单具有结帐按钮,该按钮将表单数据发送到服务器,并在单击时将客户重定向到/checkout路线。

接下来,您需要创建一条路由,以接收这些客户详细信息,并将其用于Rapyd上的create a customer。打开index.js并添加以下路线:

// Route to receive client details and create a customer on Rapyd
app.post('/', async (req, res) => {
    //create a customer using the payment details from the request body
    try {
    const body = {
      name: req.body.customerDetails.name,
      email: req.body.customerDetails.email,
      phone_number: req.body.customerDetails.phone,
      payment_method: {
     type: 'us_debit_visa_card',
     fields: {
         number: req.body.customerDetails.cardNo,
         expiration_month: req.body.customerDetails.cardExpMonth,
         expiration_year: req.body.customerDetails.cardExpYear,
         cvv: req.body.customerDetails.cardCVV
     }
      }
    }
    const { body: { data } }  = await makeRequest('POST', '/v1/customers', body)

    await storage.setItem('customerId', data.id)
    await storage.setItem('defaultPaymentMethod', data.default_payment_method)
    await storage.setItem('customerEmail', data.email)

    res.status(200).json({ success: true })

    } catch (error) {
      console.log(error)
    }
})

上面的代码使用默认付款方式创建客户。从API响应中,它提取客户ID,默认付款方式和客户电子邮件,并将详细信息保存到本地存储中。

创建结帐页面

Rapyd Checkout可作为[托管页面] https://docs.rapyd.net/en/hosted-checkout-page-integration.html)和toolkit提供。本教程实现了结帐工具包的集成。

要创建一个结帐页面,您首先必须向RAPYD收集带有所需字段的API提出发布请求。从API响应中,您需要提取结帐ID并将其发送到前端。对于expiration日期,请确保您添加一个将来的日期(Epoch Timestamp),该日期比今天的日期不超过一年。为此,将以下代码添加到您的index.js文件:

// Route to create a checkout page and return the checkout ID to the client
app.get('/checkout', async (req, res) => {
    let customerId = await storage.getItem('customerId')
    let productPrice = await storage.getItem('productPrice')

    try {
     const body = {
         amount: productPrice,
         country: 'US',
         currency: 'USD',
         customer: customerId,
         language: 'en',
         expiration: 1704019927,
     };

     const { body: { data }} = await makeRequest('POST', '/v1/checkout', body);

     res.render('checkout', {
         checkoutId: data.id
      })

      } catch (error) {
     console.error('Error completing request', error);
      }
})

下一

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Checkout Page</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
  <body class="container-sm text-center">
    <div class="cont">
     <h1>Checkout page</h1>

     <!-- iframe -->
     <div id="rapyd-checkout"></div>
    </div>

    <!-- code to display the iframe -->
    <script src="https://sandboxcheckouttoolkit.rapyd.net"></script>
    <script>
     window.onload = function () {
         let checkout = new RapydCheckoutToolkit({
             pay_button_text: "Click to pay",
             pay_button_color: "blue",
             id: "<%=checkoutId%>"
         });
         checkout.displayCheckout();
     }
     window.addEventListener('onCheckoutPaymentSuccess', function (event) {
         console.log(event.detail)
         console.log(typeof(event.detail.status))
         alert('payment successful')

         window.location.assign('http://localhost:3000/verification')

     });
     window.addEventListener('onCheckoutPaymentFailure', function (event) {
         console.log(event.detail.error)
         alert('error while processing your payment')
     });
    </script>
  </body>
</html>

上面的代码使用服务器的checkoutId来渲染一个结帐页面,其中包含客户的详细信息和要付款的金额。

成功付款时,该页面显示了一条警报消息:“付款成功”。当客户点击 OK 按钮时,它们将重定向到验证页面。

创建验证页面

在此页面上,客户需要执行3DS身份验证。由于本指南涵盖了在沙箱中工作,因此只会模拟3DS身份验证。仅在付款方式为card的情况下,3DS身份验证才是相关的。 Rapyd 3DS模拟器有助于对卡进行身份验证,以便可以完成付款。

要在客户导航时渲染验证页面,请在index.js文件中添加以下路由:

app.get('/verification', async (req, res) => {
    let customerId = await storage.getItem('customerId')
    let defaultPaymentMethod = await storage.getItem('defaultPaymentMethod')

    // Create subscription
    try {
     const subscriptionBody = {
         customer: customerId,
         billing: 'pay_automatically',
         billing_cycle_anchor: '',
         cancel_at_period_end: true,
         coupon: '',
         days_until_due: null,
         payment_method: defaultPaymentMethod,
         subscription_items: [
         {
             plan: planId,
             quantity: 1
         }
         ],
         tax_percent: 10.5,
         plan_token: ''
     };

     const { body: { data }} = await makeRequest('POST', '/v1/payments/subscriptions', subscriptionBody);

     await storage.setItem('subscriptionId', data.id)

     // create invoice
     try {
         let subscriptionId = await storage.getItem('subscriptionId')
         const invoiceBody = {
             customer: customerId,
             billing: 'pay_automatically',
             days_until_due: null,
             description: '',
             due_date: 0,
             metadata: {
             merchant_defined: true
             },
             statement_descriptor: '',
             subscription: subscriptionId,
             tax_percent: '',
             currency: 'USD'
         };
         const { body: { data }} = await makeRequest('POST', '/v1/invoices', invoiceBody);

         await storage.setItem('invoiceId', data.id)

         console.log(data);

         // create invoice items
         try {
             let customerId = await storage.getItem('customerId')
             let invoiceId = await storage.getItem('invoiceId')
             let subscriptionId = await storage.getItem('subscriptionId')
        let productPrice = await storage.getItem('productPrice')

             const invoiceItemBody = {
                 currency: 'USD',
                 customer: customerId,
                 invoice: invoiceId,
                 plan: planId,
                 metadata: {
                 merchant_defined: true
                 },
                 amount: productPrice
             };
             const { body: { data }} = await makeRequest('POST', '/v1/invoice_items', invoiceItemBody);

             console.log(data);
             } catch (error) {
             console.error('Error completing request', error);
             }
         } catch (error) {
         console.error('Error completing request', error);
         }
         } catch (error) {
     console.error('Error completing request', error);
      }

    // finalize invoice
    let invoiceId = await storage.getItem('invoiceId')
    try {
     const { body: { data }} = await makeRequest(
         'POST',
         `/v1/invoices/${invoiceId}/finalize`
     );

     console.log(data);

     res.render('verification', {
         authLink: data.payment.redirect_url
     })

      } catch (error) {
     console.error('Error completing request', error);
      }
})

在上面的代码中,您:

  • Create a subscription并将subscriptionId从API响应中保存在本地存储中。
  • Create an invoice使用subscriptionId,并将invoiceId从API响应中保存在本地存储中。
    • 注意:发票具有一些状态,如响应参数所讨论的here所讨论的。您刚刚创建的发票处于草稿状态。在此状态下,您可以编辑发票并添加发票项目。稍后,您将最终确定发票,以将其标记为“付费”并完成与之相关的付款。
  • 使用invoiceID Create invoice items并将其添加到发票中。
  • Finalize the invoice通过使用invoiceId提出帖子请求。从收到的API响应中,您提取redirect_url来模拟3DS身份验证并将其发送到前端。

接下来,您需要创建客户可以用来模拟3DS身份验证的verification页面。在views文件夹中创建一个verification.ejs文件,并添加以下代码:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Verification Page</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
  <body class="container-sm text-center">
    <div class="cont">
     <h1>3DS Verification</h1>
     <!-- <p>Open the link below in a new page to simulate 3DS authentication</p> -->
     <p>
         <a href="<%=authLink%>" target="_blank" id="auth">Simulate 3DS Authentication</a>
     </p>
     <p>Once you're done, come back to this page and select "Generate Invoice".</p>
     <button class="btn btn-primary" id="generateBtn" disabled>Generate Invoice</button>
    </div>

    <script>
     const generateBtn = document.getElementById('generateBtn')

     document.getElementById('auth').addEventListener('click', () => {
         generateBtn.removeAttribute('disabled')
     })

     generateBtn.addEventListener('click', () => {
         window.location.assign('http://localhost:3000/invoice')
     })
    </script>
  </body>
</html>

上面的代码呈现一个HTML文件,该文件指示客户单击显示的链接,执行3DS模拟,然后返回页面,然后单击生成发票按钮。此按钮将客户重定向到发票页面。

创建发票页面

此页面将向客​​户显示发票数据。要创建它,您首先需要服务器代码来检索发票和发票项目。打开index.js并添加以下路线:

// Retrieve invoice, line items => return line items to client
app.get('/invoice', async (req, res) => {
    let custEmail = await storage.getItem('customerEmail')

    let item
    let quantity
    let subTotal
    let tax
    let total
    let status

    // retrieve invoice
    try {
     let invoiceId = await storage.getItem('invoiceId')

     console.log(invoiceId)

     const { body: { data }} = await makeRequest(
         'GET',
         `/v1/invoices/${invoiceId}`
     );

     console.log(data)

     quantity = data.lines.length
     subTotal = data.subtotal
     tax = data.tax
     total = data.total
     status = data.status

     // retrieve subscription
     try {
         const subscriptionId = await storage.getItem('subscriptionId')
         const { body: { data }} = await makeRequest(
             'GET',
             `/v1/payments/subscriptions/${subscriptionId}`
         );

         item = data.subscription_items.data[0].plan.product.name;

         // console.log(data.subscription_items.data[0].plan.product)

         } catch (error) {
         console.error('Error completing request', error);
         }
      } catch (error) {
     console.error('Error completing request', error);
      }

      // send email
      let invoiceId = await storage.getItem('invoiceId')
      var data = {
      from: `Monthly Booking Services <${process.env.MAILGUN_SENDER_EMAIL}>`,
      to: custEmail,
      subject: 'Monthly Booking Services Invoice',
      html: `<h1>Monthly Booking Services</h1><h3>Your Invoice ID: ${invoiceId}</h3><p>Item: ${item}</p><p>Quantity: ${quantity}</p><p>Tax: ${tax}</p><p>Total: ${total}</p><p>Invoice status: ${status}</p>`
      };

      mailgun.messages().send(data, function (error, body) {
     if(error){
         console.log(error)
     }
      console.log(body);
      });

      res.render('invoice', {
     item,
     quantity,
     subTotal,
     tax,
     total,
     status,
     custEmail
      })
})

上面的代码检索发票和与发票关联的订阅,以提取客户订阅的产品名称。然后将所有这些信息返回到前端。该代码还包括通过MailGun将发票详细信息发送给客户电子邮件的逻辑。

views文件夹中创建一个名为invoice.ejs的文件,并添加以下代码以渲染向客户显示发票数据的页面:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Invoice Page</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
  <body class="container-sm text-center">
    <div class="cont">
     <h2>Your Invoice</h2>
     <table class="table">
         <thead>
             <tr>
             <th scope="col">#</th>
             <th scope="col">Item</th>
             <th scope="col">Quantity</th>
             <th scope="col">Rate</th>
             </tr>
         </thead>
         <tbody>
             <tr>
             <th scope="row">1</th>
             <td><%=item%></td>
             <td><%=quantity%></td>
             <td>$<%=subTotal%></td>
             </tr>

             <tr>
             <td colspan="3" class="text-end">Sub Total</td>
             <td>$<%=subTotal%></td>
             </tr>
             <tr>
             <td colspan="3" class="text-end">Tax</td>
             <td>$<%=tax%></td>
             </tr>
             <tr>
             <td colspan="3" class="text-end">Total</td>
             <td>$<%=total%></td>
             </tr>
             <tr>
             <td colspan="3" class="text-end">Invoice Status</td>
             <td class="bg-success text-white"><%=status%></td>
             </tr>
         </tbody>
         </table>

         <p class="bg-primary text-white py-4 px-2">Invoice successfully sent to: <%=custEmail%></p>
    </div>
  </body>
</html>

测试应用程序

要确认该应用程序正在按预期工作,请在终端中运行npm start,然后在浏览器上打开http://localhost:3000。您应该查看产品页面:

Product page

当您单击现在的书籍按钮时,显示了包含某些占位符值以进行测试目的的客户详细信息表格:

Full product page

输入客户电子邮件(请确保使用您用来注册Mailgun帐户的个人电子邮件),然后单击 Checkout 按钮。您将被重定向到结帐页面。在此处后,选择单击以付费按钮,然后在成功付款后单击警报上的 ok

Checkout page

这将把您重定向到验证页面。选择模拟3DS身份验证

Verification page

这打开了一个新标签。输入提供的代码,然后单击继续按钮。

3DS simulator page

导航返回验证页面,然后选择生成发票。这将把您重定向到发票页面,并通知您已将电子邮件发送给客户的电子邮件。

Invoice page

打开收件箱,您应该看到收到的电子邮件。

An image showing the received email

结论

在本文中,您了解了Rapyd和Rapyd Collect API。您还了解了各种Rapyd收集的概念,例如产品,计划,订阅,发票和发票项目。最后,您学会了如何组合所有这些概念以创建一个Web应用程序,该应用程序一旦通过邮件服务完成付款,就可以自动将发票发送给客户。

Rapyd是一种付款处理解决方案,可帮助您使用各地的付款,支出和业务所需的所有工具来解放全球贸易。它的目的是通过简化在不同国家和货币中接受和付款的过程来使企业更容易在全球扩展。 Rapyd的开发人员友好的API允许企业轻松地将其支付系统与Rapyd Network集成。