2022-09-12 (C) Questetra, Inc. (MIT License) 2 https://support.questetra.com/bpmn-icons/service-task-stripe-invoice-item-add/ https://support.questetra.com/ja/bpmn-icons/service-task-stripe-invoice-item-add/ This item adds an invoice item to a draft invoice on Stripe. この工程は、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'); // 請求書 ID が保存されている文字型データ項目(単一行)を準備し、設定 const invoiceIdDef = engine.createDataDefinition('請求書 ID', 1, 'q_invoiceId', 'STRING_TEXTFIELD'); engine.setData(invoiceIdDef, invoiceId); configs.putObject('conf_InvoiceId', invoiceIdDef); // 商品価格 ID が保存されている文字型データ項目(単一行)を準備し、設定 const priceIdDef = engine.createDataDefinition('商品価格 ID', 2, 'q_priceId', 'STRING_TEXTFIELD'); engine.setData(priceIdDef, priceId); configs.putObject('conf_PriceId', priceIdDef); // 数量が保存されている数値型データ項目を準備し、設定 const quantityDef = engine.createDataDefinition('数量', 4, 'q_quantity', 'DECIMAL'); configs.putObject('conf_Quantity', quantityDef); if (quantity !== null) { engine.setData(quantityDef, java.math.BigDecimal.valueOf(quantity)); } }; /** * 請求書 ID が空 */ test('Invoice ID is blank', () => { prepareConfigsWithPriceId(null, 'price_00001', 1); expect(execute).toThrow('Invoice ID is blank.'); }); /** * 価格 ID が空 */ test('Price ID is blank', () => { prepareConfigsWithPriceId('in_00001', null, 1); expect(execute).toThrow('Price ID is blank.'); }); /** * 価格 ID が設定されているのに、商品名も設定されている */ test('Both Price ID and item name are set', () => { prepareConfigsWithPriceId('in_00001', 'price_00001', 1); configs.put('conf_ItemName', '商品名'); expect(execute).toThrow('Price ID and item name cannot be set at the same time.'); }); /** * 価格 ID が設定されているのに、単価も設定されている */ test('Both Price ID and unit amount are set', () => { prepareConfigsWithPriceId('in_00001', 'price_00001', 1); const unitAmountDef = engine.createDataDefinition('単価', 3, 'q_unitAmount', 'DECIMAL'); configs.putObject('conf_UnitAmount', unitAmountDef); expect(execute).toThrow('Price ID and unit amount cannot be set at the same time.'); }); /** * 数量が空 */ test('Quantity is blank', () => { prepareConfigsWithPriceId('in_00001', 'price_00001', null); expect(execute).toThrow('Quantity is blank.'); }); /** * 数量が整数でない */ test('Quantity is not integer', () => { prepareConfigsWithPriceId('in_00001', 'price_00001', 1.5); expect(execute).toThrow('Quantity must be integer.'); }); /** * 数量が負の整数 */ test('Quantity is negative', () => { prepareConfigsWithPriceId('in_00001', 'price_00001', -1); expect(execute).toThrow('Quantity must not be negative.'); }); /** * 請求書オブジェクトを取得する API リクエストのテスト * @param {Object} request * @param request.url * @param request.method * @param request.headers * @param invoiceId */ const assertGet = ({url, method, headers}, invoiceId) => { expect(url).toEqual(`https://api.stripe.com/v1/invoices/${encodeURIComponent(invoiceId)}`); expect(method).toEqual('GET'); expect(headers.get('Stripe-Version')).toEqual(STRIPE_VERSION); }; /** * 請求書オブジェクトを取得する HTTP リクエストで失敗 */ test('Fail to get invoice', () => { prepareConfigsWithPriceId('in_00001', 'price_0011', 1); httpClient.setRequestHandler((request) => { assertGet(request, 'in_00001'); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); expect(execute).toThrow('Failed to get invoice. status: 400'); }); /** * 請求書ドラフトにラインアイテムを追加する API リクエストのテスト - 商品価格 ID を指定する場合 * @param {Object} request * @param request.url * @param request.method * @param request.headers * @param request.contentType * @param request.body * @param customerId * @param invoiceId * @param priceId * @param quantity */ const assertPostWithPriceId = ({url, method, headers, contentType, body}, customerId, invoiceId, priceId, quantity) => { expect(url).toEqual('https://api.stripe.com/v1/invoiceitems'); expect(method).toEqual('POST'); expect(headers.get('Stripe-Version')).toEqual(STRIPE_VERSION); expect(contentType).startsWith('application/x-www-form-urlencoded'); const expectedBody = `customer=${encodeURIComponent(customerId)}` + `&invoice=${encodeURIComponent(invoiceId)}` + `&price=${encodeURIComponent(priceId)}` + `&quantity=${quantity}`; expect(body).toEqual(expectedBody); }; /** * 請求書ドラフトにラインアイテムを追加する HTTP リクエストで失敗 */ test('Fail to attach an invoice item', () => { prepareConfigsWithPriceId('in_00001', 'price_00011', 1); let reqCount = 0; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertGet(request, 'in_00001'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{"customer": "cus_00111"}'); } assertPostWithPriceId(request, 'cus_00111', 'in_00001', 'price_00011', 1); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); expect(execute).toThrow('Failed to attach an invoice item. status: 400'); }); /** * 成功 - 商品価格 ID を文字型データ項目で指定した場合 */ test('Success - With Price ID set by string-type data item', () => { prepareConfigsWithPriceId('in_00001', 'price_00011', 1); let reqCount = 0; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertGet(request, 'in_00001'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{"customer": "cus_00111"}'); } assertPostWithPriceId(request, 'cus_00111', 'in_00001', 'price_00011', 1); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); }); /** * 成功 - 商品価格 ID を選択型データ項目で指定した場合 */ test('Success - With Price ID set by select-type data item', () => { prepareConfigsWithPriceId('in_00002', null, 1); // 選択型データ項目を準備し、設定 const priceId = 'price_00012'; const idDef = engine.createDataDefinition('価格 ID を選択', 5, 'q_priceIdSelect', 'SELECT_SINGLE'); const select = new java.util.ArrayList(); const item = engine.createItem(priceId, `${priceId} を選択`); select.add(item); engine.setData(idDef, select); configs.putObject('conf_PriceId', idDef); let reqCount = 0; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertGet(request, 'in_00002'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{"customer": "cus_00112"}'); } assertPostWithPriceId(request, 'cus_00112', 'in_00002', 'price_00012', 1); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); }); /** * 商品価格 ID を選択型データ項目で指定し、選択されていない */ test('Price ID is set by select-type data item and not selected', () => { prepareConfigsWithPriceId('in_00003', 'dummyString', 1); // 選択型データ項目を準備し、未選択のまま設定 const idDef = engine.createDataDefinition('価格 ID を選択', 5, 'q_priceIdSelect', 'SELECT_SINGLE'); configs.putObject('conf_PriceId', idDef); expect(execute).toThrow('Price ID is not selected.'); }); /** * 成功 - 商品価格 ID と数量を固定値で指定した場合 */ test('Success - With Price ID and quantity set as fixed value', () => { prepareConfigsWithPriceId('in_00004', 'dummyString', 1); configs.put('conf_PriceId', 'price_00014'); configs.put('conf_Quantity', '103'); let reqCount = 0; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertGet(request, 'in_00004'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{"customer": "cus_00114"}'); } assertPostWithPriceId(request, 'cus_00114', 'in_00004', 'price_00014', 103); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); }); /** * 数量を固定値で指定し、値が非負整数でない */ test('Quantity is set as fixed value and it is not a non-negative integer', () => { prepareConfigsWithPriceId('in_00005', 'price_00015', 1); configs.put('conf_Quantity', '1.03'); expect(execute).toThrow('Quantity must be a non-negative integer.'); }); /// 商品名と単価を設定する場合 const NAME_MAX_LENGTH = 250; const MAX_UNIT_AMOUNT = 99999999; /** * 指定の長さの文字列を作成 * @param length * @return string */ const createString = (length) => { const sourceStr = 'あいうえおかきくけこ'; const string = sourceStr.repeat(Math.floor(length / sourceStr.length)) + sourceStr.slice(0, length % sourceStr.length); return string; } /** * 設定の準備 - 商品名と単価を指定する場合 * @param invoiceId * @param itemName * @param unitAmount * @param quantity */ const prepareConfigsWithTempItem = (invoiceId, itemName, unitAmount, quantity) => { configs.put('conf_Auth', 'Stripe'); // 請求書 ID が保存されている文字型データ項目(単一行)を準備し、設定 const invoiceIdDef = engine.createDataDefinition('請求書 ID', 1, 'q_invoiceId', 'STRING_TEXTFIELD'); engine.setData(invoiceIdDef, invoiceId); configs.putObject('conf_InvoiceId', invoiceIdDef); configs.put('conf_ItemName', itemName); // 単価が保存されている数値型データ項目を準備し、設定 const unitAmountDef = engine.createDataDefinition('単価', 3, 'q_unitAmount', 'DECIMAL'); configs.putObject('conf_UnitAmount', unitAmountDef); if (unitAmount !== null) { engine.setData(unitAmountDef, java.math.BigDecimal.valueOf(unitAmount)); } // 数量が保存されている数値型データ項目を準備し、設定 const quantityDef = engine.createDataDefinition('数量', 4, 'q_quantity', 'DECIMAL'); configs.putObject('conf_Quantity', quantityDef); if (quantity !== null) { engine.setData(quantityDef, java.math.BigDecimal.valueOf(quantity)); } }; /** * 商品名が空 */ test('Item name is blank', () => { prepareConfigsWithTempItem('in_00001', '', 100, 1); expect(execute).toThrow('Item name is blank. It is required when Price ID is not set.'); }); /** * 商品名が長すぎる */ test('Item name is too long', () => { const name = createString(NAME_MAX_LENGTH + 1); prepareConfigsWithTempItem('in_00001', name, 100, 1); expect(execute).toThrow(`Item name must be at most ${NAME_MAX_LENGTH} characters.`); }); /** * 単価が設定されていない */ test('Unit amount is not set', () => { prepareConfigsWithTempItem('in_00001', '商品名', null, 1); configs.put('conf_UnitAmount', ''); // 単価の設定を解除 expect(execute).toThrow('Unit amount is not set. It is required when Price ID is not set.'); }); /** * 単価の値が空 */ test('Unit amount is blank', () => { prepareConfigsWithTempItem('in_00001', '商品名', null, 1); expect(execute).toThrow('Unit amount is blank.'); }); /** * 単価が整数でない */ test('Unit amount is not integer', () => { prepareConfigsWithTempItem('in_00001', '商品名', 100.1, 1); expect(execute).toThrow('Unit amount must be integer.'); }); /** * 単価が負の整数 */ test('Unit amount is negative', () => { prepareConfigsWithTempItem('in_00001', '商品名', -100, 1); expect(execute).toThrow('Unit amount must not be negative.'); }); /** * 単価が上限値を超える */ test('Unit amount exceeds the limit', () => { prepareConfigsWithTempItem('in_00001', '商品名', MAX_UNIT_AMOUNT + 1, 1); expect(execute).toThrow(`Unit amount must be smaller than ${MAX_UNIT_AMOUNT + 1}.`); }); /** * 請求書ドラフトにラインアイテムを追加する API リクエストのテスト - 商品名と単価を指定する場合 * @param {Object} request * @param request.url * @param request.method * @param request.headers * @param request.contentType * @param request.body * @param customerId * @param invoiceId * @param itemName * @param unitAmount * @param quantity */ const assertPostWithTempItem = ({url, method, headers, contentType, body}, customerId, invoiceId, itemName, unitAmount, quantity) => { expect(url).toEqual('https://api.stripe.com/v1/invoiceitems'); expect(method).toEqual('POST'); expect(headers.get('Stripe-Version')).toEqual(STRIPE_VERSION); expect(contentType).startsWith('application/x-www-form-urlencoded'); let expectedBody = `customer=${encodeURIComponent(customerId)}` + `&invoice=${encodeURIComponent(invoiceId)}` + `&description=${encodeURIComponent(itemName)}` + `&unit_amount=${unitAmount}` + `&quantity=${quantity}`; expectedBody = expectedBody.replace(/%20/g, '+'); // HttpRequestWrapper#formParam() はスペースを + に置き換える expect(body).toEqual(expectedBody); }; /** * 成功 - 商品名と単価を指定した場合 */ test('Success - With item name and unit amount', () => { prepareConfigsWithTempItem('in_00010', 'テスト商品 1', 500, 2); let reqCount = 0; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertGet(request, 'in_00010'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{"customer": "cus_00112"}'); } assertPostWithTempItem(request, 'cus_00112', 'in_00010', 'テスト商品 1', 500, 2); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); }); /** * 成功 - 商品名を最大文字数で、単価を最大値で指定 */ test('Success - With item name at its max length and unit amount at its max value', () => { const name = createString(NAME_MAX_LENGTH); prepareConfigsWithTempItem('in_00011', name, MAX_UNIT_AMOUNT, 10); let reqCount = 0; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertGet(request, 'in_00011'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{"customer": "cus_00122"}'); } assertPostWithTempItem(request, 'cus_00122', 'in_00011', name, MAX_UNIT_AMOUNT, 10); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); }); /** * 成功 - 単価と数量の小数点以下が 0 */ test('Success - Unit amount and quantity are decimal but the numbers after the decimal points are zeros', () => { prepareConfigsWithTempItem('in_00100', 'テスト商品 2', 900.00, 100.0); let reqCount = 0; httpClient.setRequestHandler((request) => { if (reqCount === 0) { assertGet(request, 'in_00100'); reqCount++; return httpClient.createHttpResponse(200, 'application/json', '{"customer": "cus_00113"}'); } assertPostWithTempItem(request, 'cus_00113', 'in_00100', 'テスト商品 2', 900, 100); return httpClient.createHttpResponse(200, 'application/json', '{}'); }); execute(); }); ]]>