2022-10-03 (C) Questetra, Inc. (MIT License) 2 https://support.questetra.com/bpmn-icons/service-task-stripe-product-create/ https://support.questetra.com/ja/bpmn-icons/service-task-stripe-product-create/ This item creates a product object on Stripe, with a price object attached to it. この工程は、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= { const sourceStr = 'あいうえおかきくけこ'; const string = sourceStr.repeat(Math.floor(length / sourceStr.length)) + sourceStr.slice(0, length % sourceStr.length); return string; } /** * 設定の準備 - サブ通貨を設定しない場合 * @param name * @param description * @param currency * @param unitAmount * @return returnObj * @return returnObj.productIdDef * @return returnObj.productUrlDef * @return returnObj.priceIdDef * @return returnObj.priceUrlDef */ const prepareConfigs = (name, description, currency, unitAmount) => { configs.put('conf_Auth', 'Stripe'); configs.put('conf_Name', name); configs.put('conf_Description', description); configs.put('conf_Currency', currency); // 単価を保存した数値型データ項目を準備し、設定 const unitAmountDef = engine.createDataDefinition('単価', 1, 'q_unitAmount', 'DECIMAL'); configs.putObject('conf_UnitAmount', unitAmountDef); if (unitAmount !== null) { engine.setData(unitAmountDef, java.math.BigDecimal.valueOf(unitAmount)); } // 商品オブジェクトの ID を保存する文字型データ項目(単一行)を準備し、設定 const productIdDef = engine.createDataDefinition('商品 ID', 2, 'q_productId', 'STRING_TEXTFIELD'); engine.setData(productIdDef, '事前文字列'); configs.putObject('conf_ProductId', productIdDef); // 商品オブジェクトの URL を保存する文字型データ項目(単一行)を準備し、設定 const productUrlDef = engine.createDataDefinition('商品 URL', 3, 'q_productUrl', 'STRING_TEXTFIELD'); engine.setData(productUrlDef, '事前文字列'); configs.putObject('conf_ProductUrl', productUrlDef); // 価格オブジェクトの ID を保存する文字型データ項目(単一行)を準備し、設定 const priceIdDef = engine.createDataDefinition('価格 ID', 4, 'q_priceId', 'STRING_TEXTFIELD'); engine.setData(priceIdDef, '事前文字列'); configs.putObject('conf_PriceId', priceIdDef); // 価格オブジェクトの URL を保存する文字型データ項目(単一行)を準備し、設定 const priceUrlDef = engine.createDataDefinition('価格 URL', 5, 'q_priceUrl', 'STRING_TEXTFIELD'); engine.setData(priceUrlDef, '事前文字列'); configs.putObject('conf_PriceUrl', priceUrlDef); return {productIdDef, productUrlDef, priceIdDef, priceUrlDef}; } /** * 商品名が空 */ test('Product Name is blank', () => { prepareConfigs('', '商品の説明', 'jpy', 100); expect(execute).toThrow('Product Name is blank.'); }); /** * 商品名が長すぎる */ test('Product Name is too long', () => { const name = createString(NAME_MAX_LENGTH + 1); prepareConfigs(name, '商品の説明', 'jpy', 100); expect(execute).toThrow(`Product Name must be at most ${NAME_MAX_LENGTH} characters.`); }); /** * 商品説明が長すぎる */ test('Product Description is too long', () => { const description = createString(DESCRIPTION_MAX_LENGTH + 1); prepareConfigs('テスト商品 1', description, 'jpy', 100); expect(execute).toThrow(`Product Description must be at most ${DESCRIPTION_MAX_LENGTH} characters.`); }); /** * 単価が空 */ test('Unit amount is blank', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', null); expect(execute).toThrow('Unit amount is blank.'); }); /** * 単価が整数でない */ test('Unit amount is not integer', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', 100.01); expect(execute).toThrow('Unit amount must be integer.'); }); /** * 単価が負の整数 */ test('Unit amount is negative', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', -100); expect(execute).toThrow('Unit amount must not be negative.'); }); /** * 単価が上限値を超える */ test('Unit amount exceeds the limit', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', MAX_UNIT_AMOUNT + 1); expect(execute).toThrow(`Unit amount must be smaller than ${MAX_UNIT_AMOUNT + 1}.`); }); /** * サブ通貨を設定する * @param i * @param currency * @param unitAmount */ const setSubCurrency = (i, currency, unitAmount) => { configs.put(`conf_Opt${i}_Currency`, currency); // 単価を保存した数値型データ項目を準備し、設定 const unitAmountDef = engine.createDataDefinition(`サブ通貨 ${i} の単価`, 5 + i, `q_unitAmountForOpt${i}`, 'DECIMAL'); configs.putObject(`conf_Opt${i}_UnitAmount`, unitAmountDef); if (unitAmount !== null) { engine.setData(unitAmountDef, java.math.BigDecimal.valueOf(unitAmount)); } } /** * サブ通貨が選択されていないのに単価が設定されている */ test('Unit amount for a sub-currency is set while the sub-currency is not selected', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', 100); setSubCurrency(1, '', 200); expect(execute).toThrow('Unit amount for sub-currency 1 is set while sub-currency 1 is not selected.'); }); /** * サブ通貨の指定がメイン通貨と重複 */ test('Sub-currency is the same as the main currency', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', 100); setSubCurrency(2, 'jpy', 200); expect(execute).toThrow('Currency JPY is set multiple times.'); }); /** * サブ通貨同士で指定が重複 */ test('Sub-currency is the same as another sub-currency', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', 100); setSubCurrency(1, 'usd', 200); setSubCurrency(3, 'usd', 250); expect(execute).toThrow('Currency USD is set multiple times.'); }); /** * サブ通貨の単価が設定されていない */ test('Unit amount for a sub-currency is not set', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', 100); configs.put('conf_Opt1_Currency', 'usd'); expect(execute).toThrow('Unit amount for sub-currency 1 is not set.'); }); /** * サブ通貨の単価が不正 */ test('Unit amount for a sub-currency is invalid', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', 100); setSubCurrency(4, 'usd', 200.5); expect(execute).toThrow('Unit amount for sub-currency 4 must be integer.'); }); /** * 商品オブジェクトを作成する API リクエストのテスト * @param {Object} request * @param request.url * @param request.method * @param request.headers * @param request.contentType * @param request.body * @param name * @param description * @param currency * @param unitAmount * @param currencyOptions */ const assertRequest = ({url, method, headers, contentType, body}, name, description, currency, unitAmount, currencyOptions) => { expect(url).toEqual('https://api.stripe.com/v1/products'); expect(method).toEqual('POST'); expect(headers.get('Stripe-Version')).toEqual(STRIPE_VERSION); expect(contentType).startsWith('application/x-www-form-urlencoded'); let expectedBody = `name=${encodeURIComponent(name)}` + `&${encodeURIComponent('default_price_data[currency]')}=${currency}` + `&${encodeURIComponent('default_price_data[unit_amount]')}=${unitAmount}`; if (description !== '') { expectedBody += `&description=${encodeURIComponent(description)}`; } currencyOptions.forEach((subUnitAmount, subCurrency) => { const encodedParamName = encodeURIComponent(`default_price_data[currency_options][${subCurrency}][unit_amount]`); expectedBody += `&${encodedParamName}=${subUnitAmount}`; }); expectedBody = expectedBody.replace(/%20/g, '+'); // HttpRequestWrapper#formParam() はスペースを + に置き換える expect(body).toEqual(expectedBody); }; const NO_CURRENCY_OPTIONS = new Map(); /** * 商品オブジェクトを作成する HTTP リクエストで失敗 */ test('Fail to create product', () => { prepareConfigs('テスト商品 1', '商品の説明', 'jpy', 100); httpClient.setRequestHandler((request) => { assertRequest(request, 'テスト商品 1', '商品の説明', 'jpy', 100, NO_CURRENCY_OPTIONS); return httpClient.createHttpResponse(400, 'application/json', '{}'); }); expect(execute).toThrow('Failed to create product. status: 400'); }); /** * 成功 - JPY, 単価は整数, 商品説明あり, サブ通貨なし */ test('Success - JPY, Unit amount is integer, With description, No sub-currencies', () => { const {productIdDef, productUrlDef, priceIdDef, priceUrlDef} = prepareConfigs('テスト商品 1', '商品 1 の説明', 'jpy', 100); const productObj = { 'id': 'prod_10001', 'default_price': 'price_00001' }; httpClient.setRequestHandler((request) => { assertRequest(request, 'テスト商品 1', '商品 1 の説明', 'jpy', 100, NO_CURRENCY_OPTIONS); return httpClient.createHttpResponse(200, 'application/json', JSON.stringify(productObj)); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(productIdDef)).toEqual('prod_10001'); expect(engine.findData(productUrlDef)).toEqual('https://dashboard.stripe.com/products/prod_10001'); expect(engine.findData(priceIdDef)).toEqual('price_00001'); expect(engine.findData(priceUrlDef)).toEqual('https://dashboard.stripe.com/prices/price_00001'); }); /** * 成功 - USD, 単価の小数点以下が 0, 商品説明なし, サブ通貨を 1 つ指定 */ test('Success - USD, Unit amount can be parsed to integer, Without description, With a sub-currency', () => { const {productIdDef, productUrlDef, priceIdDef, priceUrlDef} = prepareConfigs('テスト商品 2', '', 'usd', 500.00); setSubCurrency(1, 'cad', 400.00); const currencyOptions = new Map(); currencyOptions.set('cad', 400); const productObj = { 'id': 'prod_10002', 'default_price': 'price_00002' }; httpClient.setRequestHandler((request) => { assertRequest(request, 'テスト商品 2', '', 'usd', 500, currencyOptions); return httpClient.createHttpResponse(200, 'application/json', JSON.stringify(productObj)); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(productIdDef)).toEqual('prod_10002'); expect(engine.findData(productUrlDef)).toEqual('https://dashboard.stripe.com/products/prod_10002'); expect(engine.findData(priceIdDef)).toEqual('price_00002'); expect(engine.findData(priceUrlDef)).toEqual('https://dashboard.stripe.com/prices/price_00002'); }); /** * 成功 - KRW, 商品名と商品説明の長さが最大, 単価が最大値, サブ通貨を 2 つ指定 */ test('Success - KRW, String configs with longest length, Unit amount at its maximum, With two sub-currencies', () => { const name = createString(NAME_MAX_LENGTH); const description = createString(DESCRIPTION_MAX_LENGTH); const {productIdDef, productUrlDef, priceIdDef, priceUrlDef} = prepareConfigs(name, description, 'krw', MAX_UNIT_AMOUNT); setSubCurrency(2, 'hkd', MAX_UNIT_AMOUNT); setSubCurrency(4, 'jpy', MAX_UNIT_AMOUNT - 1); const currencyOptions = new Map(); currencyOptions.set('hkd', MAX_UNIT_AMOUNT); currencyOptions.set('jpy', MAX_UNIT_AMOUNT - 1); const productObj = { 'id': 'prod_10003', 'default_price': 'price_00003' }; httpClient.setRequestHandler((request) => { assertRequest(request, name, description, 'krw', MAX_UNIT_AMOUNT, currencyOptions); return httpClient.createHttpResponse(200, 'application/json', JSON.stringify(productObj)); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(productIdDef)).toEqual('prod_10003'); expect(engine.findData(productUrlDef)).toEqual('https://dashboard.stripe.com/products/prod_10003'); expect(engine.findData(priceIdDef)).toEqual('price_00003'); expect(engine.findData(priceUrlDef)).toEqual('https://dashboard.stripe.com/prices/price_00003'); }); /** * 成功 - 価格オブジェクトのID と URL を保存しない, サブ通貨が選択されているがデータ項目の値が空 */ test('Success - Price ID and URL not saved, sub-currency is ignored as its unit amount is blank', () => { const {productIdDef, productUrlDef, priceIdDef, priceUrlDef} = prepareConfigs('テスト商品 4', '商品 4 の説明', 'jpy', 100); configs.put('conf_PriceId', ''); configs.put('conf_PriceUrl', ''); setSubCurrency(3, 'cny', null); const productObj = { 'id': 'prod_20001', 'default_price': 'price_00004' }; httpClient.setRequestHandler((request) => { assertRequest(request, 'テスト商品 4', '商品 4 の説明', 'jpy', 100, NO_CURRENCY_OPTIONS); return httpClient.createHttpResponse(200, 'application/json', JSON.stringify(productObj)); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(productIdDef)).toEqual('prod_20001'); expect(engine.findData(productUrlDef)).toEqual('https://dashboard.stripe.com/products/prod_20001'); expect(engine.findData(priceIdDef)).toEqual('事前文字列'); expect(engine.findData(priceUrlDef)).toEqual('事前文字列'); }); /** * 成功 - 商品オブジェクトのID と URL を保存しない, サブ通貨を 4 つすべてに設定 */ test('Success - Product ID and URL not saved, all sub-currencies are set', () => { const {productIdDef, productUrlDef, priceIdDef, priceUrlDef} = prepareConfigs('テスト商品 5', '商品 5 の説明', 'aud', 700); configs.put('conf_ProductId', ''); configs.put('conf_ProductUrl', ''); setSubCurrency(1, 'usd', 100); setSubCurrency(2, 'cad', 200); setSubCurrency(3, 'gbp', 300); setSubCurrency(4, 'eur', 400); const currencyOptions = new Map(); currencyOptions.set('usd', 100); currencyOptions.set('cad', 200); currencyOptions.set('gbp', 300); currencyOptions.set('eur', 400); const productObj = { 'id': 'prod_20002', 'default_price': 'price_00005' }; httpClient.setRequestHandler((request) => { assertRequest(request, 'テスト商品 5', '商品 5 の説明', 'aud', 700, currencyOptions); return httpClient.createHttpResponse(200, 'application/json', JSON.stringify(productObj)); }); execute(); // 文字型データ項目の値をチェック expect(engine.findData(productIdDef)).toEqual('事前文字列'); expect(engine.findData(productUrlDef)).toEqual('事前文字列'); expect(engine.findData(priceIdDef)).toEqual('price_00005'); expect(engine.findData(priceUrlDef)).toEqual('https://dashboard.stripe.com/prices/price_00005'); }); ]]>