diff --git a/packages/js/api-core-tests/data/index.js b/packages/js/api-core-tests/data/index.js index 2bfd95b1b2e..a264c745f15 100644 --- a/packages/js/api-core-tests/data/index.js +++ b/packages/js/api-core-tests/data/index.js @@ -1,7 +1,9 @@ -const { order, getOrderExample } = require('./order'); -const { coupon } = require('./coupon'); -const { refund } = require('./refund'); -const shared = require('./shared'); +const { order, getOrderExample } = require( './order' ); +const { coupon } = require( './coupon' ); +const { refund } = require( './refund' ); +const { getExampleTaxRate } = require( './tax-rate' ); +const { getExampleVariation } = require( './variation' ); +const shared = require( './shared' ); module.exports = { order, @@ -9,4 +11,6 @@ module.exports = { coupon, shared, refund, + getExampleTaxRate, + getExampleVariation, }; diff --git a/packages/js/api-core-tests/data/tax-rate.js b/packages/js/api-core-tests/data/tax-rate.js new file mode 100644 index 00000000000..98449e58eea --- /dev/null +++ b/packages/js/api-core-tests/data/tax-rate.js @@ -0,0 +1,21 @@ +/** + * A standard tax rate. + * + * For more details on the tax rate properties, see: + * + * https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-rate-properties + * + */ +const taxRate = { + name: 'Standard Rate', + rate: '10.0000', + class: 'standard', +}; + +const getExampleTaxRate = () => { + return taxRate; +}; + +module.exports = { + getExampleTaxRate, +}; diff --git a/packages/js/api-core-tests/data/variation.js b/packages/js/api-core-tests/data/variation.js new file mode 100644 index 00000000000..46fab09a5cf --- /dev/null +++ b/packages/js/api-core-tests/data/variation.js @@ -0,0 +1,29 @@ +/** + * A basic product variation. + * + * For more details on the product variation properties, see: + * + * https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variations + * + */ +const variation = { + regular_price: '20.00', + attributes: [ + { + name: 'Size', + option: 'Large', + }, + { + name: 'Colour', + option: 'Red', + }, + ], +}; + +const getExampleVariation = () => { + return variation; +}; + +module.exports = { + getExampleVariation, +}; diff --git a/packages/js/api-core-tests/endpoints/index.js b/packages/js/api-core-tests/endpoints/index.js index 43953654d8c..ec922008d39 100644 --- a/packages/js/api-core-tests/endpoints/index.js +++ b/packages/js/api-core-tests/endpoints/index.js @@ -1,11 +1,15 @@ -const { ordersApi } = require('./orders'); -const { couponsApi } = require('./coupons'); -const { productsApi } = require('./products'); -const { refundsApi } = require('./refunds'); +const { ordersApi } = require( './orders' ); +const { couponsApi } = require( './coupons' ); +const { productsApi } = require( './products' ); +const { refundsApi } = require( './refunds' ); +const { taxRatesApi } = require( './tax-rates' ); +const { variationsApi } = require( './variations' ); module.exports = { ordersApi, couponsApi, productsApi, refundsApi, + taxRatesApi, + variationsApi, }; diff --git a/packages/js/api-core-tests/endpoints/tax-rates.js b/packages/js/api-core-tests/endpoints/tax-rates.js new file mode 100644 index 00000000000..ae2afa2a4e9 --- /dev/null +++ b/packages/js/api-core-tests/endpoints/tax-rates.js @@ -0,0 +1,73 @@ +/** + * Internal dependencies + */ +const { + getRequest, + postRequest, + putRequest, + deleteRequest, +} = require( '../utils/request' ); +const { getExampleTaxRate, shared } = require( '../data' ); + +/** + * WooCommerce Tax Rates endpoints. + * + * https://woocommerce.github.io/woocommerce-rest-api-docs/#tax-rates + */ +const taxRatesApi = { + name: 'Tax Rates', + create: { + name: 'Create a tax rate', + method: 'POST', + path: 'taxes', + responseCode: 201, + payload: getExampleTaxRate(), + taxRate: async ( taxRate ) => postRequest( 'taxes', taxRate ), + }, + retrieve: { + name: 'Retrieve a tax rate', + method: 'GET', + path: 'taxes/', + responseCode: 200, + taxRate: async ( taxRateId ) => taxes( `coupons/${ taxRateId }` ), + }, + listAll: { + name: 'List all tax rates', + method: 'GET', + path: 'taxes', + responseCode: 200, + taxRates: async ( queryString = {} ) => + getRequest( 'taxes', queryString ), + }, + update: { + name: 'Update a tax rate', + method: 'PUT', + path: 'taxes/', + responseCode: 200, + payload: getExampleTaxRate(), + taxRate: async ( taxRateId, taxRateDetails ) => + putRequest( `taxes/${ taxRateId }`, taxRateDetails ), + }, + delete: { + name: 'Delete a tax rate', + method: 'DELETE', + path: 'taxes/', + responseCode: 200, + payload: { + force: false, + }, + taxRate: async ( taxRateId, deletePermanently ) => + deleteRequest( `taxes/${ taxRateId }`, deletePermanently ), + }, + batch: { + name: 'Batch update tax rates', + method: 'POST', + path: 'taxes/batch', + responseCode: 200, + payload: shared.getBatchPayloadExample( getExampleTaxRate() ), + taxRates: async ( batchUpdatePayload ) => + postRequest( `taxes/batch`, batchUpdatePayload ), + }, +}; + +module.exports = { taxRatesApi }; diff --git a/packages/js/api-core-tests/endpoints/variations.js b/packages/js/api-core-tests/endpoints/variations.js new file mode 100644 index 00000000000..68584fa52fd --- /dev/null +++ b/packages/js/api-core-tests/endpoints/variations.js @@ -0,0 +1,84 @@ +/** + * Internal dependencies + */ +const { + getRequest, + postRequest, + putRequest, + deleteRequest, +} = require( '../utils/request' ); +const { getExampleVariation, shared } = require( '../data' ); + +/** + * WooCommerce Product Variation endpoints. + * + * https://woocommerce.github.io/woocommerce-rest-api-docs/#product-variations + */ +const variationsApi = { + name: 'Product variations', + create: { + name: 'Create a product variation', + method: 'POST', + path: 'products//variations', + responseCode: 201, + payload: getExampleVariation(), + variation: async ( productId, variation ) => + postRequest( `products/${ productId }/variations`, variation ), + }, + retrieve: { + name: 'Retrieve a product variation', + method: 'GET', + path: 'products//variations/', + responseCode: 200, + variation: async ( productId, variationId ) => + `products/${ productId }/variations/${ variationId }`, + }, + listAll: { + name: 'List all product variations', + method: 'GET', + path: 'products//variations', + responseCode: 200, + variations: async ( productId, queryString = {} ) => + getRequest( `products/${ productId }/variations`, queryString ), + }, + update: { + name: 'Update a product variation', + method: 'PUT', + path: 'products//variations/', + responseCode: 200, + payload: getExampleVariation(), + variation: async ( productId, variationId, variationDetails ) => + putRequest( + `products/${ productId }/variations/${ variationId }`, + taxRateDetails + ), + }, + delete: { + name: 'Delete a product variation', + method: 'DELETE', + path: 'products//variations/', + responseCode: 200, + payload: { + force: false, + }, + variation: async ( productId, variationId, deletePermanently ) => + deleteRequest( + `products/${ productId }/variations/${ variationId }`, + deletePermanently + ), + }, + batch: { + name: 'Batch update product variations', + method: 'POST', + path: 'products//variations/batch', + responseCode: 200, + payload: shared.getBatchPayloadExample( getExampleVariation() ), + variations: async ( batchUpdatePayload ) => + postRequest( + `products/${ productId }/variations/${ variationId }`, + batchUpdatePayload + ), + }, +}; + +module.exports = { variationsApi }; diff --git a/packages/js/api-core-tests/tests/orders/order-complex.test.js b/packages/js/api-core-tests/tests/orders/order-complex.test.js new file mode 100644 index 00000000000..3519509aae6 --- /dev/null +++ b/packages/js/api-core-tests/tests/orders/order-complex.test.js @@ -0,0 +1,240 @@ +const { + taxRatesApi, + productsApi, + ordersApi, + variationsApi, +} = require( '../../endpoints' ); +const { getOrderExample, getExampleTaxRate } = require( '../../data' ); + +/** + * Simple product with Standard tax rate + */ +const simpleProduct = { + name: 'Black Compact Keyboard', + regular_price: '10.00', + tax_class: 'standard', +}; + +/** + * Variable product with 1 variation with Reduced tax rate + */ +const variableProduct = { + name: 'Unbranded Granite Shirt', + type: 'variable', + tax_class: 'reduced-rate', + defaultAttributes: [ + { + name: 'Size', + option: 'Medium', + }, + { + name: 'Colour', + option: 'Blue', + }, + ], + attributes: [ + { + name: 'Colour', + visible: true, + variation: true, + options: [ 'Red', 'Green', 'Blue' ], + }, + { + name: 'Size', + visible: true, + variation: true, + options: [ 'Small', 'Medium', 'Large' ], + }, + { + name: 'Logo', + visible: true, + variation: true, + options: [ 'Woo', 'WordPress' ], + }, + ], +}; +const variation = { + regular_price: '20.00', + tax_class: 'reduced-rate', + attributes: [ + { + name: 'Size', + option: 'Large', + }, + { + name: 'Colour', + option: 'Red', + }, + ], +}; + +/** + * External product with Zero rate tax + */ +const externalProduct = { + name: 'Ergonomic Steel Computer', + regular_price: '400.00', + type: 'external', + tax_class: 'zero-rate', +}; + +/** + * Grouped product with 1 linked product + */ +const groupedProduct = { + name: 'Full Modern Computer Set', + type: 'grouped', +}; + +/** + * Tax rates for each tax class + */ +const standardTaxRate = getExampleTaxRate(); +const reducedTaxRate = { + name: 'Reduced Rate', + rate: '1.0000', + class: 'reduced-rate', +}; +const zeroTaxRate = { + name: 'Zero Rate', + rate: '0.0000', + class: 'zero-rate', +}; + +/** + * Expected totals + */ +const expectedOrderTotal = '442.20'; +const expectedTaxTotal = '2.20'; +const expectedSimpleProductTaxTotal = '1.00'; +const expectedVariableProductTaxTotal = '0.20'; +const expectedExternalProductTaxTotal = '0.00'; + +let order; + +/** + * + * Test for adding a complex order with different product types and tax classes. + * + * @group orders + * @group tax_rates + */ +describe( 'Orders API test', () => { + beforeAll( async () => { + // Create a tax rate for each tax class, and save their ID's + const { body: createdStandardRate } = await taxRatesApi.create.taxRate( + standardTaxRate + ); + standardTaxRate.id = createdStandardRate.id; + + const { body: createdReducedRate } = await taxRatesApi.create.taxRate( + reducedTaxRate + ); + reducedTaxRate.id = createdReducedRate.id; + + const { body: createdZeroRate } = await taxRatesApi.create.taxRate( + zeroTaxRate + ); + zeroTaxRate.id = createdZeroRate.id; + + // Create a simple product + const { body: createdSimpleProduct } = await productsApi.create.product( + simpleProduct + ); + simpleProduct.id = createdSimpleProduct.id; + + // Link this simple product to a grouped product + groupedProduct.grouped_products = [ simpleProduct.id ]; + + // Create a variable product with 1 variation + const { + body: createdVariableProduct, + } = await productsApi.create.product( variableProduct ); + variableProduct.id = createdVariableProduct.id; + await variationsApi.create.variation( variableProduct.id, variation ); + + // Create a grouped product + const { + body: createdGroupedProduct, + } = await productsApi.create.product( groupedProduct ); + groupedProduct.id = createdGroupedProduct.id; + + // Create an external product + const { + body: createdExternalProduct, + } = await productsApi.create.product( externalProduct ); + externalProduct.id = createdExternalProduct.id; + } ); + + afterAll( async () => { + // Delete order + await ordersApi.delete.order( order.id, true ); + + // Delete products + await productsApi.batch.products( { + delete: [ + simpleProduct.id, + variableProduct.id, + externalProduct.id, + groupedProduct.id, + ], + } ); + + // Delete tax rates + await taxRatesApi.batch.taxRates( { + delete: [ standardTaxRate.id, zeroTaxRate.id, reducedTaxRate.id ], + } ); + } ); + + it( 'can add complex order', async () => { + // Create an order with products having different tax classes + const createOrderPayload = { + ...getOrderExample(), + shipping_lines: [], + fee_lines: [], + coupon_lines: [], + line_items: [ + { product_id: simpleProduct.id }, + { product_id: variableProduct.id }, + { product_id: externalProduct.id }, + { product_id: groupedProduct.id }, + ], + }; + const { status, body } = await ordersApi.create.order( + createOrderPayload + ); + order = body; + + expect( status ).toEqual( ordersApi.create.responseCode ); + + // Verify totals + expect( body.total ).toEqual( expectedOrderTotal ); + expect( body.total_tax ).toEqual( expectedTaxTotal ); + + // Verify tax total of each product line item + const actualSimpleProductLineItem = body.line_items.find( + ( { product_id } ) => product_id === simpleProduct.id + ); + const actualVariableProductLineItem = body.line_items.find( + ( { product_id } ) => product_id === variableProduct.id + ); + const actualGroupedProductLineItem = body.line_items.find( + ( { product_id } ) => product_id === groupedProduct.id + ); + const actualExternalProductLineItem = body.line_items.find( + ( { product_id } ) => product_id === externalProduct.id + ); + expect( actualSimpleProductLineItem.total_tax ).toEqual( + expectedSimpleProductTaxTotal + ); + expect( actualGroupedProductLineItem.total_tax ).toEqual( + expectedSimpleProductTaxTotal + ); + expect( actualVariableProductLineItem.total_tax ).toEqual( + expectedVariableProductTaxTotal + ); + expect( actualExternalProductLineItem.total_tax ).toEqual( + expectedExternalProductTaxTotal + ); + } ); +} );