Merge pull request #31338 from woocommerce/fix/28166-coupon-item-meta-data

Consistently Store Coupon Data in Order Item Meta
This commit is contained in:
Peter Fabian 2021-12-07 18:35:08 +01:00 committed by GitHub
commit edcffa9912
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 178 additions and 123 deletions

View File

@ -1,6 +1,6 @@
const { couponsApi } = require('../../endpoints/coupons'); const { couponsApi } = require( '../../endpoints/coupons' );
const { ordersApi } = require('../../endpoints/orders'); const { ordersApi } = require( '../../endpoints/orders' );
const { coupon, order } = require('../../data'); const { coupon, order } = require( '../../data' );
/** /**
* Tests for the WooCommerce Coupons API. * Tests for the WooCommerce Coupons API.
@ -9,37 +9,39 @@ const { coupon, order } = require('../../data');
* @group coupons * @group coupons
* *
*/ */
describe('Coupons API tests', () => { describe( 'Coupons API tests', () => {
let couponId; let couponId;
it('can create a coupon', async () => { it( 'can create a coupon', async () => {
const testCoupon = { const testCoupon = {
...coupon, ...coupon,
code: `${coupon.code}-${Date.now()}`, code: `${ coupon.code }-${ Date.now() }`,
}; };
const response = await couponsApi.create.coupon(testCoupon); const response = await couponsApi.create.coupon( testCoupon );
expect(response.statusCode).toEqual(couponsApi.create.responseCode); expect( response.statusCode ).toEqual( couponsApi.create.responseCode );
expect(response.body.id).toBeDefined(); expect( response.body.id ).toBeDefined();
couponId = response.body.id; couponId = response.body.id;
// Validate the created coupon object has the correct code, amount, and discount type // Validate the created coupon object has the correct code, amount, and discount type
expect(response.body).toEqual( expect( response.body ).toEqual(
expect.objectContaining({ expect.objectContaining( {
code: testCoupon.code, code: testCoupon.code,
amount: Number(coupon.amount).toFixed(2), amount: Number( coupon.amount ).toFixed( 2 ),
discount_type: coupon.discount_type, discount_type: coupon.discount_type,
}) } )
); );
}); } );
it('can retrieve a coupon', async () => { it( 'can retrieve a coupon', async () => {
const response = await couponsApi.retrieve.coupon(couponId); const response = await couponsApi.retrieve.coupon( couponId );
expect(response.statusCode).toEqual(couponsApi.retrieve.responseCode); expect( response.statusCode ).toEqual(
expect(response.body.id).toEqual(couponId); couponsApi.retrieve.responseCode
}); );
expect( response.body.id ).toEqual( couponId );
} );
it('can update a coupon', async () => { it( 'can update a coupon', async () => {
const updatedCouponDetails = { const updatedCouponDetails = {
description: '10% off storewide', description: '10% off storewide',
maximum_amount: '500.00', maximum_amount: '500.00',
@ -50,40 +52,40 @@ describe('Coupons API tests', () => {
couponId, couponId,
updatedCouponDetails updatedCouponDetails
); );
expect(response.statusCode).toEqual(couponsApi.update.responseCode); expect( response.statusCode ).toEqual( couponsApi.update.responseCode );
expect(response.body).toEqual( expect( response.body ).toEqual(
expect.objectContaining(updatedCouponDetails) expect.objectContaining( updatedCouponDetails )
); );
}); } );
it('can permanently delete a coupon', async () => { it( 'can permanently delete a coupon', async () => {
const response = await couponsApi.delete.coupon(couponId, true); const response = await couponsApi.delete.coupon( couponId, true );
expect(response.statusCode).toEqual(couponsApi.delete.responseCode); expect( response.statusCode ).toEqual( couponsApi.delete.responseCode );
const getCouponResponse = await couponsApi.retrieve.coupon(couponId); const getCouponResponse = await couponsApi.retrieve.coupon( couponId );
expect(getCouponResponse.statusCode).toEqual(404); expect( getCouponResponse.statusCode ).toEqual( 404 );
}); } );
describe('Batch update coupons', () => { describe( 'Batch update coupons', () => {
/** /**
* Coupons to be created, updated, and deleted. * Coupons to be created, updated, and deleted.
*/ */
const expectedCoupons = [ const expectedCoupons = [
{ {
code: `batchcoupon-${Date.now()}`, code: `batchcoupon-${ Date.now() }`,
discount_type: 'percent', discount_type: 'percent',
amount: '10', amount: '10',
free_shipping: false, free_shipping: false,
}, },
{ {
code: `batchcoupon-${Date.now() + 1}`, code: `batchcoupon-${ Date.now() + 1 }`,
discount_type: 'percent', discount_type: 'percent',
amount: '20', amount: '20',
}, },
]; ];
it('can batch create coupons', async () => { it( 'can batch create coupons', async () => {
// Batch create 2 new coupons. // Batch create 2 new coupons.
const batchCreatePayload = { const batchCreatePayload = {
create: expectedCoupons, create: expectedCoupons,
@ -91,36 +93,36 @@ describe('Coupons API tests', () => {
const batchCreateResponse = await couponsApi.batch.coupons( const batchCreateResponse = await couponsApi.batch.coupons(
batchCreatePayload batchCreatePayload
); );
expect(batchCreateResponse.status).toEqual( expect( batchCreateResponse.status ).toEqual(
couponsApi.batch.responseCode couponsApi.batch.responseCode
); );
// Verify that the 2 new coupons were created // Verify that the 2 new coupons were created
const actualCoupons = batchCreateResponse.body.create; const actualCoupons = batchCreateResponse.body.create;
expect(actualCoupons).toHaveLength(expectedCoupons.length); expect( actualCoupons ).toHaveLength( expectedCoupons.length );
for (let i = 0; i < actualCoupons.length; i++) { for ( let i = 0; i < actualCoupons.length; i++ ) {
const { id, code } = actualCoupons[i]; const { id, code } = actualCoupons[ i ];
const expectedCouponCode = expectedCoupons[i].code; const expectedCouponCode = expectedCoupons[ i ].code;
expect(id).toBeDefined(); expect( id ).toBeDefined();
expect(code).toEqual(expectedCouponCode); expect( code ).toEqual( expectedCouponCode );
// Save the coupon id // Save the coupon id
expectedCoupons[i].id = id; expectedCoupons[ i ].id = id;
} }
}); } );
it('can batch update coupons', async () => { it( 'can batch update coupons', async () => {
// Update the 1st coupon to free shipping. // Update the 1st coupon to free shipping.
// Update the amount of the 2nd coupon to 25. // Update the amount of the 2nd coupon to 25.
const batchUpdatePayload = { const batchUpdatePayload = {
update: [ update: [
{ {
id: expectedCoupons[0].id, id: expectedCoupons[ 0 ].id,
free_shipping: true, free_shipping: true,
}, },
{ {
id: expectedCoupons[1].id, id: expectedCoupons[ 1 ].id,
amount: '25.00', amount: '25.00',
}, },
], ],
@ -131,23 +133,23 @@ describe('Coupons API tests', () => {
// Verify the response code and the number of coupons that were updated. // Verify the response code and the number of coupons that were updated.
const updatedCoupons = batchUpdateResponse.body.update; const updatedCoupons = batchUpdateResponse.body.update;
expect(batchUpdateResponse.status).toEqual( expect( batchUpdateResponse.status ).toEqual(
couponsApi.batch.responseCode couponsApi.batch.responseCode
); );
expect(updatedCoupons).toHaveLength(expectedCoupons.length); expect( updatedCoupons ).toHaveLength( expectedCoupons.length );
// Verify that the 1st coupon was updated to free shipping. // Verify that the 1st coupon was updated to free shipping.
expect(updatedCoupons[0].id).toEqual(expectedCoupons[0].id); expect( updatedCoupons[ 0 ].id ).toEqual( expectedCoupons[ 0 ].id );
expect(updatedCoupons[0].free_shipping).toEqual(true); expect( updatedCoupons[ 0 ].free_shipping ).toEqual( true );
// Verify that the amount of the 2nd coupon was updated to 25. // Verify that the amount of the 2nd coupon was updated to 25.
expect(updatedCoupons[1].id).toEqual(expectedCoupons[1].id); expect( updatedCoupons[ 1 ].id ).toEqual( expectedCoupons[ 1 ].id );
expect(updatedCoupons[1].amount).toEqual('25.00'); expect( updatedCoupons[ 1 ].amount ).toEqual( '25.00' );
}); } );
it('can batch delete coupons', async () => { it( 'can batch delete coupons', async () => {
// Batch delete the 2 coupons. // Batch delete the 2 coupons.
const couponIdsToDelete = expectedCoupons.map(({ id }) => id); const couponIdsToDelete = expectedCoupons.map( ( { id } ) => id );
const batchDeletePayload = { const batchDeletePayload = {
delete: couponIdsToDelete, delete: couponIdsToDelete,
}; };
@ -157,151 +159,171 @@ describe('Coupons API tests', () => {
// Verify that the response shows the 2 coupons. // Verify that the response shows the 2 coupons.
const deletedCouponIds = batchDeleteResponse.body.delete.map( const deletedCouponIds = batchDeleteResponse.body.delete.map(
({ id }) => id ( { id } ) => id
); );
expect(batchDeleteResponse.status).toEqual( expect( batchDeleteResponse.status ).toEqual(
couponsApi.batch.responseCode couponsApi.batch.responseCode
); );
expect(deletedCouponIds).toEqual(couponIdsToDelete); expect( deletedCouponIds ).toEqual( couponIdsToDelete );
// Verify that the 2 deleted coupons cannot be retrieved. // Verify that the 2 deleted coupons cannot be retrieved.
for (const couponId of couponIdsToDelete) { for ( const couponId of couponIdsToDelete ) {
const { status } = await couponsApi.retrieve.coupon(couponId); const { status } = await couponsApi.retrieve.coupon( couponId );
expect(status).toEqual(404); expect( status ).toEqual( 404 );
} }
}); } );
}); } );
describe('List coupons', () => { describe( 'List coupons', () => {
const allCoupons = [ const allCoupons = [
{ {
...coupon, ...coupon,
code: `listcoupons-01-${Date.now()}`, code: `listcoupons-01-${ Date.now() }`,
description: `description-01-${Date.now()}`, description: `description-01-${ Date.now() }`,
}, },
{ {
...coupon, ...coupon,
code: `listcoupons-02-${Date.now()}`, code: `listcoupons-02-${ Date.now() }`,
description: `description-02-${Date.now()}`, description: `description-02-${ Date.now() }`,
}, },
{ {
...coupon, ...coupon,
code: `listcoupons-03-${Date.now()}`, code: `listcoupons-03-${ Date.now() }`,
description: `description-03-${Date.now()}`, description: `description-03-${ Date.now() }`,
}, },
]; ];
beforeAll(async () => { beforeAll( async () => {
// Create list of coupons for testing. // Create list of coupons for testing.
const response = await couponsApi.batch.coupons({ const response = await couponsApi.batch.coupons( {
create: allCoupons, create: allCoupons,
}); } );
const actualCreatedCoupons = response.body.create; const actualCreatedCoupons = response.body.create;
// Save their coupon ID's // Save their coupon ID's
for (const coupon of allCoupons) { for ( const coupon of allCoupons ) {
const { id } = actualCreatedCoupons.find( const { id } = actualCreatedCoupons.find(
({ code }) => coupon.code === code ( { code } ) => coupon.code === code
); );
coupon.id = id; coupon.id = id;
} }
}); } );
afterAll(async () => { afterAll( async () => {
// Clean up created coupons // Clean up created coupons
const batchDeletePayload = { const batchDeletePayload = {
delete: allCoupons.map(({ id }) => id), delete: allCoupons.map( ( { id } ) => id ),
}; };
await couponsApi.batch.coupons(batchDeletePayload); await couponsApi.batch.coupons( batchDeletePayload );
}); } );
it('can list all coupons by default', async () => { it( 'can list all coupons by default', async () => {
const response = await couponsApi.listAll.coupons(); const response = await couponsApi.listAll.coupons();
const listedCoupons = response.body; const listedCoupons = response.body;
const actualCouponIdsList = listedCoupons.map(({ id }) => id); const actualCouponIdsList = listedCoupons.map( ( { id } ) => id );
const expectedCouponIdsList = allCoupons.map(({ id }) => id); const expectedCouponIdsList = allCoupons.map( ( { id } ) => id );
expect(response.status).toEqual(couponsApi.listAll.responseCode); expect( response.status ).toEqual(
expect(actualCouponIdsList).toEqual( couponsApi.listAll.responseCode
expect.arrayContaining(expectedCouponIdsList)
); );
}); expect( actualCouponIdsList ).toEqual(
expect.arrayContaining( expectedCouponIdsList )
);
} );
it('can limit result set to matching code', async () => { it( 'can limit result set to matching code', async () => {
const matchingCoupon = allCoupons[1]; const matchingCoupon = allCoupons[ 1 ];
const payload = { code: matchingCoupon.code }; const payload = { code: matchingCoupon.code };
const { status, body } = await couponsApi.listAll.coupons(payload); const { status, body } = await couponsApi.listAll.coupons(
payload
);
expect(status).toEqual(couponsApi.listAll.responseCode); expect( status ).toEqual( couponsApi.listAll.responseCode );
expect(body).toHaveLength(1); expect( body ).toHaveLength( 1 );
expect(body[0].id).toEqual(matchingCoupon.id); expect( body[ 0 ].id ).toEqual( matchingCoupon.id );
}); } );
it('can paginate results', async () => { it( 'can paginate results', async () => {
const pageSize = 2; const pageSize = 2;
const payload = { const payload = {
page: 1, page: 1,
per_page: pageSize, per_page: pageSize,
}; };
const { status, body } = await couponsApi.listAll.coupons(payload); const { status, body } = await couponsApi.listAll.coupons(
payload
);
expect(status).toEqual(couponsApi.listAll.responseCode); expect( status ).toEqual( couponsApi.listAll.responseCode );
expect(body).toHaveLength(pageSize); expect( body ).toHaveLength( pageSize );
}); } );
it('can limit results to matching string', async () => { it( 'can limit results to matching string', async () => {
// Search by description // Search by description
const matchingCoupon = allCoupons[2]; const matchingCoupon = allCoupons[ 2 ];
const matchingString = matchingCoupon.description; const matchingString = matchingCoupon.description;
const payload = { const payload = {
search: matchingString, search: matchingString,
}; };
const { status, body } = await couponsApi.listAll.coupons(payload); const { status, body } = await couponsApi.listAll.coupons(
payload
);
expect(status).toEqual(couponsApi.listAll.responseCode); expect( status ).toEqual( couponsApi.listAll.responseCode );
expect(body).toHaveLength(1); expect( body ).toHaveLength( 1 );
expect(body[0].id).toEqual(matchingCoupon.id); expect( body[ 0 ].id ).toEqual( matchingCoupon.id );
}); } );
}); } );
describe('Add coupon to order', () => { describe( 'Add coupon to order', () => {
const testCoupon = { const testCoupon = {
code: `coupon-${Date.now()}`, code: `coupon-${ Date.now() }`,
discount_type: 'percent', discount_type: 'percent',
amount: '10', amount: '10',
}; };
let orderId; let orderId;
beforeAll(async () => { beforeAll( async () => {
// Create a coupon // Create a coupon
const createCouponResponse = await couponsApi.create.coupon( const createCouponResponse = await couponsApi.create.coupon(
testCoupon testCoupon
); );
testCoupon.id = createCouponResponse.body.id; testCoupon.id = createCouponResponse.body.id;
}); } );
// Clean up created coupon and order // Clean up created coupon and order
afterAll(async () => { afterAll( async () => {
await couponsApi.delete.coupon(testCoupon.id, true); await couponsApi.delete.coupon( testCoupon.id, true );
await ordersApi.delete.order(orderId, true); await ordersApi.delete.order( orderId, true );
}); } );
it('can add coupon to an order', async () => { it( 'can add coupon to an order', async () => {
const orderWithCoupon = { const orderWithCoupon = {
...order, ...order,
coupon_lines: [{ code: testCoupon.code }], coupon_lines: [ { code: testCoupon.code } ],
}; };
const { status, body } = await ordersApi.create.order( const { status, body } = await ordersApi.create.order(
orderWithCoupon orderWithCoupon
); );
orderId = body.id; orderId = body.id;
expect(status).toEqual(ordersApi.create.responseCode); expect( status ).toEqual( ordersApi.create.responseCode );
expect(body.coupon_lines).toHaveLength(1); expect( body.coupon_lines ).toHaveLength( 1 );
expect(body.coupon_lines[0].code).toEqual(testCoupon.code); expect( body.coupon_lines[ 0 ].code ).toEqual( testCoupon.code );
}); // Test that the coupon meta data exists.
}); // See: https://github.com/woocommerce/woocommerce/issues/28166.
}); expect( body.coupon_lines[ 0 ].meta_data ).toEqual(
expect.arrayContaining( [
expect.objectContaining( {
key: 'coupon_data',
value: expect.objectContaining( {
code: testCoupon.code,
} ),
} ),
] )
);
} );
} );
} );

View File

@ -1323,6 +1323,16 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
if ( ! $item_id ) { if ( ! $item_id ) {
$coupon_item = new WC_Order_Item_Coupon(); $coupon_item = new WC_Order_Item_Coupon();
$coupon_item->set_code( $coupon_code ); $coupon_item->set_code( $coupon_code );
// Add coupon data.
$coupon_id = wc_get_coupon_id_by_code( $coupon_code );
$coupon = new WC_Coupon( $coupon_id );
// Avoid storing used_by - it's not needed and can get large.
$coupon_data = $coupon->get_data();
unset( $coupon_data['used_by'] );
$coupon_item->add_meta_data( 'coupon_data', $coupon_data );
} else { } else {
$coupon_item = $this->get_item( $item_id, false ); $coupon_item = $this->get_item( $item_id, false );
} }

View File

@ -246,4 +246,27 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case {
$this->assertEquals( 0, ( new WC_Coupon( $coupon_code_2 ) )->get_usage_count() ); $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_2 ) )->get_usage_count() );
$this->assertEquals( 0, ( new WC_Coupon( $coupon_code_3 ) )->get_usage_count() ); $this->assertEquals( 0, ( new WC_Coupon( $coupon_code_3 ) )->get_usage_count() );
} }
/**
* Test apply_coupon() stores coupon meta data.
* See: https://github.com/woocommerce/woocommerce/issues/28166.
*/
public function test_apply_coupon_stores_meta_data() {
$coupon_code = 'coupon_test_meta_data';
$coupon = WC_Helper_Coupon::create_coupon( $coupon_code );
$order = WC_Helper_Order::create_order();
$order->set_status( 'processing' );
$order->save();
$order->apply_coupon( $coupon_code );
$coupon_items = $order->get_items( 'coupon' );
$this->assertCount( 1, $coupon_items );
$coupon_data = ( current( $coupon_items ) )->get_meta( 'coupon_data' );
$this->assertNotEmpty( $coupon_data, 'WC_Order_Item_Coupon missing `coupon_data` meta.' );
$this->assertArrayHasKey( 'id', $coupon_data );
$this->assertArrayHasKey( 'code', $coupon_data );
$this->assertEquals( $coupon->get_id(), $coupon_data['id'] );
$this->assertEquals( $coupon_code, $coupon_data['code'] );
}
} }