2022-07-14 (C) Questetra, Inc. (MIT License) 2 https://support.questetra.com/bpmn-icons/service-task-stripe-invoice-create/ https://support.questetra.com/ja/bpmn-icons/service-task-stripe-invoice-create/ This item creates a draft invoice on Stripe. The created invoice remains a draft until you finalize it, which allows you to send the invoice or charge the customer. この工程は、Stripe 上に請求書のドラフト(下書き)を作成します。顧客に請求書の送付やの課金を行うには、 別途、ドラフト状態の請求書を確定させる必要があります。 iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAvJJREFUWEfF l19I01EUxz/XKZqWNbVS6cH+UfSUGQlCD0EQRCkoFDkDUzENi6IgkB4sKEgjDStFnVlzalC9+BBR IEFBEeVbFP3R/lhRpLRpKHO78dtv021u+pszfnv87Zzv+dxzzj33XoHOP6E1fvleGe9KJF/CDiSZ CDIAo8d/BMkggn4BfVE27rX0ir9atOcEKC6WqdEOTkuoAOK0iALjAponY7jY0SF+zOYzK0CJSVYI uAQkaAwcaDYm4VS7VTSH8g8JUFokryOpnGdgfzdBk7lTHAmmFRSgxCS7BBxYkOAeEQnd7VZRGKg5 A2BBVz4z2oxM+AF4at60kCsP1JJQ6dsTUwBKtxscvI+g4bRyjzljWOfdHVMAZSZZL+G4VpVI7AQ0 tFnFCUXDDaAMGWciv8PY55HEV3zHDTaSlWHlBigzySIJlvmoxsbBxHj4ngIOtllFpxug1CTNQMls MjnbIS0d7t6etmpshe9DcKEmfACg3WwVpSpAoXyFIDNQZuMm2J2rfk1Ng6XL4NlTSE4Blwt6LOB0 QtEhkBKGvsCdHli9FnLzoe8h9L8MASfpN3eJLd4MDPscLFMeTTcgOhocDjXNi5fAr5+wYqUa+NOA ClF9FiYnVdtHD2DnLpiYgNhYqKmGr5+DQoyYrSLJCyCDmdQ1gtEIdpv6r7LKbgscroKr9ZC/Dzpa VIBr9VBeBYYoiDKotkLA/V7/svnGMVuFYuLugaAAx05C+ipIWa4KShe8eA7ZOdB4GQr2TwMMfoSM NfDuLazfALY/YLdD9y148zp4GXwBgpag9goYk2BkGJ48hj15MDoK8QnQUAtZ26DrJrRaVMBRO9Sd h7wC2JylAp87A9+G5ipBiCbU0ttbs6HiqFqCkA0XTCigCefchqFgFsWrO2TggxZcP5vpbRjJIAo7 rMfBbxDpPoo941i/w0gB0P04ViB0vZB4m0nXK5kXQtdL6X/JRLjXcp9M6Pcw8ULo+jTznXK6PU7n O2q1+v0D2nRCMMki7aoAAAAASUVORK5CYII= { configs.put('conf_Auth', 'Stripe'); configs.put('conf_CustomerId', customerId); configs.put('conf_Description', description); // ラインアイテムの情報を保存したテーブル型データ項目を準備し、設定 const tableDef = engine.createDataDefinition('商品名、単価、数量の一覧', 1, 'q_lineItems', 'LIST'); tableDef.addSubDataDefinition('商品名', 'q_description', 'STRING'); // 単価、数量は DECIMAL 型、小数点以下の桁数 2、小数点ピリオド、桁区切りなしで設定 tableDef.addDecimalSubDataDefinition('単価', 'q_unitAmount', 2, true, false); tableDef.addDecimalSubDataDefinition('数量', 'q_quantity', 2, true, false); configs.putObject('conf_LineItems', tableDef); if (lineItems.length > 0) { const table = tableDef.createListArray(); // ScriptListArray lineItems.forEach(lineItem => { const newRow = table.addRow(); lineItem.forEach((cellString, i) => { newRow.setCol(i, cellString); }); }); engine.setData(tableDef, table); } // 請求書ドラフトの ID を保存する文字型データ項目(単一行)を準備し、設定 const invoiceIdDef = engine.createDataDefinition('請求書 ID', 2, 'q_invoiceId', 'STRING_TEXTFIELD'); engine.setData(invoiceIdDef, '事前文字列'); configs.putObject('conf_InvoiceId', invoiceIdDef); // 請求書ドラフトの URL を保存する文字型データ項目(単一行)を準備し、設定 const invoiceUrlDef = engine.createDataDefinition('請求書 URL', 3, 'q_invoiceUrl', 'STRING_TEXTFIELD'); engine.setData(invoiceUrlDef, '事前文字列'); configs.putObject('conf_InvoiceUrl', invoiceUrlDef); return {invoiceIdDef, invoiceUrlDef}; } const SAMPLE_LINE_ITEMS = [ ['テスト商品', '50', '1'] ]; /** * 顧客 ID が空 */ test('Customer ID is blank', () => { prepareConfigs('', '請求書の説明', SAMPLE_LINE_ITEMS); expect(execute).toThrow('Customer ID is blank.'); }); /** * ラインアイテムが多すぎて HTTP リクエスト数制限を超える */ test('Too many line items', () => { const lineItems = []; for (let i = 0; i < httpClient.getRequestingLimit(); i++) { lineItems.push([`テスト商品 ${i + 1}`, '50', '1']); } prepareConfigs('cus_000001', '請求書の説明', lineItems); expect(execute).toThrow('Too many line items. Number of necessary HTTP requests exceeds the limit.'); }); /** * ラインアイテムの形式が不正 - 2 列以下 */ test('Invalid line items - too few columns', () => { prepareConfigs('cus_000001', '請求書の説明', []); // 新しいテーブル型データ項目を作成し、設定 const tableDef = engine.createDataDefinition('新しいテーブル', 4, 'q_newTable', 'LIST'); tableDef.addSubDataDefinition('商品名', 'q_description', 'STRING'); tableDef.addSubDataDefinition('単価', 'q_unitAmount', 'DECIMAL'); // 数量の列がない const table = tableDef.createListArray(); // ScriptListArray const newRow = table.addRow(); newRow.setCol(0, 'テスト商品'); newRow.setCol(1, '50'); engine.setData(tableDef, table); configs.putObject('conf_LineItems', tableDef); expect(execute).toThrow('The line items must include item description, unit amount, and quantity.'); }); /** * ラインアイテムの形式が不正 - 1 列目が文字列型でない */ test('Invalid line items - 1st column is not STRING', () => { prepareConfigs('cus_000001', '請求書の説明', []); // 新しいテーブル型データ項目を作成し、設定 const tableDef = engine.createDataDefinition('新しいテーブル', 4, 'q_newTable', 'LIST'); tableDef.addSubDataDefinition('商品名', 'q_description', 'DECIMAL'); // STRING でない tableDef.addSubDataDefinition('単価', 'q_unitAmount', 'DECIMAL'); tableDef.addSubDataDefinition('数量', 'q_quantity', 'DECIMAL'); const table = tableDef.createListArray(); // ScriptListArray const newRow = table.addRow(); newRow.setCol(0, '0'); newRow.setCol(1, '50'); newRow.setCol(2, '1'); engine.setData(tableDef, table); configs.putObject('conf_LineItems', tableDef); expect(execute).toThrow('Line item name (1st column) must be STRING.'); }); /** * ラインアイテムの形式が不正 - 2 列目が数値型でない */ test('Invalid line items - 2nd column is not DECIMAL', () => { prepareConfigs('cus_000001', '請求書の説明', []); // 新しいテーブル型データ項目を作成し、設定 const tableDef = engine.createDataDefinition('新しいテーブル', 4, 'q_newTable', 'LIST'); tableDef.addSubDataDefinition('商品名', 'q_description', 'STRING'); tableDef.addSubDataDefinition('単価', 'q_unitAmount', 'STRING'); // DECIMAL でない tableDef.addSubDataDefinition('数量', 'q_quantity', 'DECIMAL'); const table = tableDef.createListArray(); // ScriptListArray const newRow = table.addRow(); newRow.setCol(0, 'テスト商品'); newRow.setCol(1, '50'); newRow.setCol(2, '1'); engine.setData(tableDef, table); configs.putObject('conf_LineItems', tableDef); expect(execute).toThrow('Line item unit amount (2nd column) must be DECIMAL.'); }); /** * ラインアイテムの形式が不正 - 3 列目が数値型でない */ test('Invalid line items - 3rd column is not DECIMAL', () => { prepareConfigs('cus_000001', '請求書の説明', []); // 新しいテーブル型データ項目を作成し、設定 const tableDef = engine.createDataDefinition('新しいテーブル', 4, 'q_newTable', 'LIST'); tableDef.addSubDataDefinition('商品名', 'q_description', 'STRING'); tableDef.addSubDataDefinition('単価', 'q_unitAmount', 'DECIMAL'); tableDef.addSubDataDefinition('数量', 'q_quantity', 'STRING'); // DECIMAL でない const table = tableDef.createListArray(); // ScriptListArray const newRow = table.addRow(); newRow.setCol(0, 'テスト商品'); newRow.setCol(1, '50'); newRow.setCol(2, '1'); engine.setData(tableDef, table); configs.putObject('conf_LineItems', tableDef); expect(execute).toThrow('Line item quantity (3rd column) must be DECIMAL.'); }); /** * ラインアイテムの形式が不正 - 商品名が空の行がある */ test('Invalid line items - Item name is blank', () => { const lineItems = [ ['テスト商品 1', '50', '1'], ['', '150', '2'], ['テスト商品 3', '100', '3'] ]; prepareConfigs('cus_000001', '請求書の説明', lineItems); expect(execute).toThrow('Line item 2 is invalid. Item name must not be blank.'); }); /** * ラインアイテムの形式が不正 - 単価が小数 */ test('Invalid line items - Unit amount is decimal', () => { const lineItems = [ ['テスト商品 1', '50.00', '1'], // 小数点以下が 0 の行ではエラーにならない ['テスト商品 2', '150', '2'], ['テスト商品 3', '100.05', '3'] // この行でエラー ]; prepareConfigs('cus_000001', '請求書の説明', lineItems); expect(execute).toThrow('Line item 3 is invalid. Unit amount must be integer.'); }); /** * ラインアイテムの形式が不正 - 単価が負の整数 */ test('Invalid line items - Unit amount is negative', () => { const lineItems = [ ['テスト商品 1', '-50', '1'], ['テスト商品 2', '150', '2'], ['テスト商品 3', '100', '3'] ]; prepareConfigs('cus_000001', '請求書の説明', lineItems); expect(execute).toThrow('Line item 1 is invalid. Unit amount must not be negative.'); }); /** * ラインアイテムの形式が不正 - 数量が小数 */ test('Invalid line items - Quantity is decimal', () => { const lineItems = [ ['テスト商品 1', '50', '1.0'], // 小数点以下が 0 の行ではエラーにならない ['テスト商品 2', '150', '2.5'], // この行でエラー ['テスト商品 3', '100', '3'] ]; prepareConfigs('cus_000001', '請求書の説明', lineItems); expect(execute).toThrow('Line item 2 is invalid. Quantity must be integer.'); }); /** * ラインアイテムの形式が不正 - 数量が負の整数 */ test('Invalid line items - Quantity is negative', () => { const lineItems = [ ['テスト商品 1', '50', '1'], ['テスト商品 2', '150', '2'], ['テスト商品 3', '100', '-3'] ]; prepareConfigs('cus_000001', '請求書の説明', lineItems); expect(execute).toThrow('Line item 3 is invalid. Quantity must not be negative.'); }); /** * 合計額が上限を超える - ラインアイテム 1 つ */ test('Total amount exceeds the limit - one line item', () => { const lineItems = [ ['テスト商品 1', '10000', '10000'] ]; prepareConfigs('cus_000001', '請求書の説明', lineItems); expect(execute).toThrow('The total amount of line items must be less than 100000000.'); }); /** * 合計額が上限を超える - 複数のラインアイテム */ test('Total amount exceeds the limit - multiple line items', () => { const lineItems = [ ['テスト商品 1', '10000000', '9'], ['テスト商品 2', '1000000', '9'], ['テスト商品 3', '100000', '10'] // ここで上限を超える ]; prepareConfigs('cus_000001', '請求書の説明', lineItems); expect(execute).toThrow('The total amount of line items must be less than 100000000.'); }); /** * 請求書ドラフトを作成する API リクエストのテスト * @param {Object} request * @param request.url * @param request.method * @param request.contentType * @param request.body * @param customerId * @param description */ const assertCreateInvoiceRequest = ({url, method, contentType, body}, customerId, description) => { expect(url).toEqual('https://api.stripe.com/v1/invoices'); expect(method).toEqual('POST'); expect(contentType).startsWith('application/x-www-form-urlencoded'); let expectedBody = `customer=${encodeURIComponent(customerId)}` + `&description=${encodeURIComponent(description)}` + '&auto_advance=false' + '&pending_invoice_items_behavior=exclude'; expectedBody = expectedBody.replace(/%20/g, '+'); // HttpRequestWrapper#formParam() はスペースを + に置き換える expect(body).toEqual(expectedBody); }; /** * 請求書ドラフトを作成する HTTP リクエストで失敗 */ test('Fail to create invoice', () => { prepareConfigs('cus_000001', '請求書の説明', SAMPLE_LINE_ITEMS); httpClient.setRequestHandler((request) => { assertCreateInvoiceRequest(request, 'cus_000001', '請求書の説明'); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); expect(execute).toThrow('Failed to create draft invoice. status: 400'); }); /** * 成功 - ラインアイテムが 1 行もない */ test('Success - No line items', () => { const {invoiceIdDef, invoiceUrlDef} = prepareConfigs('cus_000001', '請求書の説明', []); const invoiceId = 'in_000101'; httpClient.setRequestHandler((request) => { assertCreateInvoiceRequest(request, 'cus_000001', '請求書の説明'); return httpClient.createHttpResponse(200, 'application/json', `{"id": "${invoiceId}"}`); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(invoiceIdDef)).toEqual(invoiceId); expect(engine.findData(invoiceUrlDef)).toEqual(`https://dashboard.stripe.com/invoices/${invoiceId}`); }); /** * 成功 - ラインアイテムをあらわすテーブル型データ項目の設定なし */ test('Success - No list-type data item is set', () => { const {invoiceIdDef, invoiceUrlDef} = prepareConfigs('cus_000001', '請求書の説明', SAMPLE_LINE_ITEMS); configs.put('conf_LineItems', ''); const invoiceId = 'in_000102'; httpClient.setRequestHandler((request) => { assertCreateInvoiceRequest(request, 'cus_000001', '請求書の説明'); return httpClient.createHttpResponse(200, 'application/json', `{"id": "${invoiceId}"}`); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(invoiceIdDef)).toEqual(invoiceId); expect(engine.findData(invoiceUrlDef)).toEqual(`https://dashboard.stripe.com/invoices/${invoiceId}`); }); /** * 請求書ドラフトにラインアイテムを追加する API リクエストのテスト * @param {Object} request * @param request.url * @param request.method * @param request.contentType * @param request.body * @param customerId * @param invoiceId * @param lineItem */ const assertAttachInvoiceItemRequest = ({url, method, contentType, body}, customerId, invoiceId, lineItem) => { expect(url).toEqual('https://api.stripe.com/v1/invoiceitems'); expect(method).toEqual('POST'); expect(contentType).startsWith('application/x-www-form-urlencoded'); let expectedBody = `customer=${encodeURIComponent(customerId)}` + `&invoice=${invoiceId}` + `&description=${encodeURIComponent(lineItem[0])}` + `&unit_amount=${lineItem[1]}` + `&quantity=${lineItem[2]}`; expectedBody = expectedBody.replace(/%20/g, '+'); // HttpRequestWrapper#formParam() はスペースを + に置き換える expect(body).toEqual(expectedBody); }; /** * 請求書ドラフトにラインアイテムを追加する HTTP リクエストで失敗 */ test('Fail to attach an invoice item', () => { prepareConfigs('cus_000001', '請求書の説明', SAMPLE_LINE_ITEMS); let reqCount = 0; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertCreateInvoiceRequest(request, 'cus_000001', '請求書の説明'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{"id": "in_000001"}'); } assertAttachInvoiceItemRequest(request, 'cus_000001', 'in_000001', SAMPLE_LINE_ITEMS[0]); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); expect(execute).toThrow('Failed to attach an invoice item. status: 400'); }); /** * 成功 - ラインアイテムが 1 つの場合 */ test('Success - one line item', () => { const {invoiceIdDef, invoiceUrlDef} = prepareConfigs('cus_000001', '請求書の説明', SAMPLE_LINE_ITEMS); let reqCount = 0; const invoiceId = 'in_000001'; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertCreateInvoiceRequest(request, 'cus_000001', '請求書の説明'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', `{"id": "${invoiceId}"}`); } assertAttachInvoiceItemRequest(request, 'cus_000001', invoiceId, SAMPLE_LINE_ITEMS[0]); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(invoiceIdDef)).toEqual(invoiceId); expect(engine.findData(invoiceUrlDef)).toEqual(`https://dashboard.stripe.com/invoices/${invoiceId}`); }); /** * 成功 - ラインアイテムが複数で、合計額が上限を超えないぎりぎりの値の場合 */ test('Success - multiple line items, maximum total amount', () => { const lineItems = [ // 合計額が上限を超えないぎりぎりの値になるように ['テスト商品 1', '10000000', '9'], ['テスト商品 2', '1000000', '9'], ['テスト商品 3', '999999', '1'] ]; const {invoiceIdDef, invoiceUrlDef} = prepareConfigs('cus_000002', '上限額ぎりぎりの請求書', lineItems); let reqCount = 0; let itemCount = 0; const invoiceId = 'in_000002'; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertCreateInvoiceRequest(request, 'cus_000002', '上限額ぎりぎりの請求書'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', `{"id": "${invoiceId}"}`); } assertAttachInvoiceItemRequest(request, 'cus_000002', invoiceId, lineItems[itemCount]); itemCount++; return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(invoiceIdDef)).toEqual(invoiceId); expect(engine.findData(invoiceUrlDef)).toEqual(`https://dashboard.stripe.com/invoices/${invoiceId}`); }); /** * 成功 - ラインアイテムが複数で、HTTP リクエスト数上限を超えないぎりぎりの数の場合 */ test('Success - maximum number of line items', () => { const lineItems = []; for (let i = 0; i < httpClient.getRequestingLimit() - 1; i++) { lineItems.push([`テスト商品 ${i + 1}`, '50', '1']); } const {invoiceIdDef, invoiceUrlDef} = prepareConfigs('cus_000001', '上限個数ぎりぎりの請求書', lineItems); let reqCount = 0; let itemCount = 0; const invoiceId = 'in_000003'; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertCreateInvoiceRequest(request, 'cus_000001', '上限個数ぎりぎりの請求書'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', `{"id": "${invoiceId}"}`); } assertAttachInvoiceItemRequest(request, 'cus_000001', invoiceId, lineItems[itemCount]); itemCount++; return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(invoiceIdDef)).toEqual(invoiceId); expect(engine.findData(invoiceUrlDef)).toEqual(`https://dashboard.stripe.com/invoices/${invoiceId}`); }); /** * 成功 - 請求書の説明が空の場合 */ test('Success - invoice description is blank', () => { const {invoiceIdDef, invoiceUrlDef} = prepareConfigs('cus_000001', '', SAMPLE_LINE_ITEMS); let reqCount = 0; const invoiceId = 'in_000004'; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertCreateInvoiceRequest(request, 'cus_000001', ''); reqCount++; return httpClient.createHttpResponse(200, 'application/json', `{"id": "${invoiceId}"}`); } assertAttachInvoiceItemRequest(request, 'cus_000001', invoiceId, SAMPLE_LINE_ITEMS[0]); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(invoiceIdDef)).toEqual(invoiceId); expect(engine.findData(invoiceUrlDef)).toEqual(`https://dashboard.stripe.com/invoices/${invoiceId}`); }); /** * 成功 - 請求書ドラフトの ID, URL を保存しない場合 */ test('Success - draft invoice ID and URL not saved', () => { prepareConfigs('cus_000003', 'ID と URL を保存しない', SAMPLE_LINE_ITEMS); configs.put('conf_InvoiceId', ''); configs.put('conf_InvoiceUrl', ''); let reqCount = 0; const invoiceId = 'in_000005'; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertCreateInvoiceRequest(request, 'cus_000003', 'ID と URL を保存しない'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', `{"id": "${invoiceId}"}`); } assertAttachInvoiceItemRequest(request, 'cus_000003', invoiceId, SAMPLE_LINE_ITEMS[0]); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); }); ]]>