diff --git a/app/DoctrineMigrations/Version20241129223912.php b/app/DoctrineMigrations/Version20241129223912.php new file mode 100644 index 0000000000..976f114ad4 --- /dev/null +++ b/app/DoctrineMigrations/Version20241129223912.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE store DROP create_orders'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE store ADD create_orders BOOLEAN NOT NULL default true'); + } +} diff --git a/cypress/e2e/local-commerce/bookmarks.cy.js b/cypress/e2e/local-commerce/@admin/bookmarks.cy.js similarity index 63% rename from cypress/e2e/local-commerce/bookmarks.cy.js rename to cypress/e2e/local-commerce/@admin/bookmarks.cy.js index cd7ec44f79..e1845920fd 100644 --- a/cypress/e2e/local-commerce/bookmarks.cy.js +++ b/cypress/e2e/local-commerce/@admin/bookmarks.cy.js @@ -26,46 +26,12 @@ context('Bookmarks (Saved orders) (role: admin)', () => { .click() // Pickup + cy.chooseSavedPickupAddress(1) - cy.searchAddress( - '[data-form="task"]:nth-of-type(1)', - '23 Avenue Claude Vellefaux, 75010 Paris, France', - /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, - ) - - cy.get('#delivery_tasks_0_address_name__display') - .clear() - cy.get('#delivery_tasks_0_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') - cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - - cy.searchAddress( - '[data-form="task"]:nth-of-type(2)', - '72 Rue Saint-Maur, 75011 Paris, France', - /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, - ) - - cy.get('#delivery_tasks_1_address_name__display') - .clear() - cy.get('#delivery_tasks_1_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type('Jane smith') + cy.chooseSavedDropoff1Address(2) cy.get('#delivery_tasks_1_weight').clear() cy.get('#delivery_tasks_1_weight').type(2.5) diff --git a/cypress/e2e/local-commerce/create_delivery_as_admin.cy.js b/cypress/e2e/local-commerce/@admin/create_delivery_as_admin.cy.js similarity index 66% rename from cypress/e2e/local-commerce/create_delivery_as_admin.cy.js rename to cypress/e2e/local-commerce/@admin/create_delivery_as_admin.cy.js index 3567fd9835..b5b1625132 100644 --- a/cypress/e2e/local-commerce/create_delivery_as_admin.cy.js +++ b/cypress/e2e/local-commerce/@admin/create_delivery_as_admin.cy.js @@ -27,46 +27,28 @@ context('Delivery (role: admin)', () => { // Pickup - cy.searchAddress( + cy.newPickupAddress( '[data-form="task"]:nth-of-type(1)', '23 Avenue Claude Vellefaux, 75010 Paris, France', /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, + 'Office', + '+33112121212', + 'John Doe', ) - cy.get('#delivery_tasks_0_address_name__display') - .clear() - cy.get('#delivery_tasks_0_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') - cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - cy.searchAddress( + cy.newDropoff1Address( '[data-form="task"]:nth-of-type(2)', '72 Rue Saint-Maur, 75011 Paris, France', /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, + 'Office', + '+33112121212', + 'Jane smith', ) - cy.get('#delivery_tasks_1_address_name__display') - .clear() - cy.get('#delivery_tasks_1_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type('Jane smith') - cy.get('#delivery_tasks_1_weight').clear() cy.get('#delivery_tasks_1_weight').type(2.5) @@ -87,6 +69,9 @@ context('Delivery (role: admin)', () => { cy.get('[data-testid=delivery__list_item]') .contains(/72,? Rue Saint-Maur,? 75011,? Paris,? France/) .should('exist') + cy.get('[data-testid=delivery__list_item]') + .contains(/€4.99/) + .should('exist') cy.get('[data-testid="delivery__list_item"]') .find('[data-testid="delivery_id"]') @@ -126,46 +111,28 @@ context('Delivery (role: admin)', () => { // Pickup - cy.searchAddress( + cy.newPickupAddress( '[data-form="task"]:nth-of-type(1)', '23 Avenue Claude Vellefaux, 75010 Paris, France', /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, + 'Office', + '+33112121212', + 'John Doe', ) - cy.get('#delivery_tasks_0_address_name__display') - .clear() - cy.get('#delivery_tasks_0_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') - cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - cy.searchAddress( + cy.newDropoff1Address( '[data-form="task"]:nth-of-type(2)', '72 Rue Saint-Maur, 75011 Paris, France', /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, + 'Office', + '+33112121212', + 'Jane smith', ) - cy.get('#delivery_tasks_1_address_name__display') - .clear() - cy.get('#delivery_tasks_1_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type('Jane smith') - cy.get('#delivery_tasks_1_weight').clear() cy.get('#delivery_tasks_1_weight').type(2.5) @@ -190,6 +157,9 @@ context('Delivery (role: admin)', () => { cy.get('[data-testid=delivery__list_item]') .contains(/72,? Rue Saint-Maur,? 75011,? Paris,? France/) .should('exist') + cy.get('[data-testid=delivery__list_item]') + .contains(/€72.00/) + .should('exist') cy.get('[data-testid="delivery__list_item"]') .find('[data-testid="delivery_id"]') @@ -230,46 +200,28 @@ context('Delivery (role: admin)', () => { // Pickup - cy.searchAddress( + cy.newPickupAddress( '[data-form="task"]:nth-of-type(1)', '23 Avenue Claude Vellefaux, 75010 Paris, France', /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, + 'Office', + '+33112121212', + 'John Doe', ) - cy.get('#delivery_tasks_0_address_name__display') - .clear() - cy.get('#delivery_tasks_0_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') - cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - cy.searchAddress( + cy.newDropoff1Address( '[data-form="task"]:nth-of-type(2)', '72 Rue Saint-Maur, 75011 Paris, France', /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, + 'Office', + '+33112121212', + 'Jane smith', ) - cy.get('#delivery_tasks_1_address_name__display') - .clear() - cy.get('#delivery_tasks_1_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type('Jane smith') - cy.get('#delivery_tasks_1_weight').clear() cy.get('#delivery_tasks_1_weight').type(2.5) @@ -309,59 +261,41 @@ context('Delivery (role: admin)', () => { .should('exist') }) - it('create delivery for store with createOrders disabled', function () { + it('create delivery for store without pricing', function () { cy.visit('/admin/stores') - cy.get('[data-testid=store_Store_with_createOrders_disabled__list_item]') + cy.get('[data-testid=store_Store_without_pricing__list_item]') .find('.dropdown-toggle') .click() - cy.get('[data-testid=store_Store_with_createOrders_disabled__list_item]') + cy.get('[data-testid=store_Store_without_pricing__list_item]') .contains('Créer une livraison') .click() // Pickup - cy.searchAddress( + cy.newPickupAddress( '[data-form="task"]:nth-of-type(1)', '23 Avenue Claude Vellefaux, 75010 Paris, France', /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, + 'Office', + '+33112121212', + 'John Doe', ) - cy.get('#delivery_tasks_0_address_name__display') - .clear() - cy.get('#delivery_tasks_0_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') - cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - cy.searchAddress( + cy.newDropoff1Address( '[data-form="task"]:nth-of-type(2)', '72 Rue Saint-Maur, 75011 Paris, France', /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, + 'Office', + '+33112121212', + 'Jane smith', ) - cy.get('#delivery_tasks_1_address_name__display') - .clear() - cy.get('#delivery_tasks_1_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type('Jane smith') - cy.get('#delivery_tasks_1_weight').clear() cy.get('#delivery_tasks_1_weight').type(2.5) @@ -379,5 +313,56 @@ context('Delivery (role: admin)', () => { cy.get('[data-testid=delivery__list_item]') .contains(/72,? Rue Saint-Maur,? 75011,? Paris,? France/) .should('exist') + cy.get('[data-testid=delivery__list_item]') + .contains(/€0.00/) + .should('exist') + }) + + it('create delivery for store with invalid pricing', function () { + cy.visit('/admin/stores') + + cy.get('[data-testid=store_Store_with_invalid_pricing__list_item]') + .find('.dropdown-toggle') + .click() + + cy.get('[data-testid=store_Store_with_invalid_pricing__list_item]') + .contains('Créer une livraison') + .click() + + // Pickup + + cy.newPickupAddress( + '[data-form="task"]:nth-of-type(1)', + '23 Avenue Claude Vellefaux, 75010 Paris, France', + /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, + 'Office', + '+33112121212', + 'John Doe', + ) + + cy.get('#delivery_tasks_0_comments').type('Pickup comments') + + // Dropoff + + cy.newDropoff1Address( + '[data-form="task"]:nth-of-type(2)', + '72 Rue Saint-Maur, 75011 Paris, France', + /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, + 'Office', + '+33112121212', + 'Jane smith', + ) + + cy.get('#delivery_tasks_1_weight').clear() + cy.get('#delivery_tasks_1_weight').type(2.5) + + cy.get('#delivery_tasks_1_comments').type('Dropoff comments') + + cy.get('#delivery-submit').click() + + cy.get('.alert-danger', { timeout: 10000 }).should( + 'contain', + "Le prix de la course n'a pas pu être calculé.", + ) }) }) diff --git a/cypress/e2e/local-commerce/recurrence_rules/create_delivery_with_recurrence_rule_as_admin.cy.js b/cypress/e2e/local-commerce/@admin/recurrence_rules/create_delivery_with_recurrence_rule_as_admin.cy.js similarity index 71% rename from cypress/e2e/local-commerce/recurrence_rules/create_delivery_with_recurrence_rule_as_admin.cy.js rename to cypress/e2e/local-commerce/@admin/recurrence_rules/create_delivery_with_recurrence_rule_as_admin.cy.js index 3f79590a5f..5cc4472ca1 100644 --- a/cypress/e2e/local-commerce/recurrence_rules/create_delivery_with_recurrence_rule_as_admin.cy.js +++ b/cypress/e2e/local-commerce/@admin/recurrence_rules/create_delivery_with_recurrence_rule_as_admin.cy.js @@ -27,46 +27,12 @@ describe('Delivery with recurrence rule (role: admin)', () => { .click() // Pickup - - cy.searchAddress( - '[data-form="task"]:nth-of-type(1)', - '23 Avenue Claude Vellefaux, 75010 Paris, France', - /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, - ) - - cy.get('#delivery_tasks_0_address_name__display').clear() - cy.get('#delivery_tasks_0_address_name__display').type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display').type( - '+33112121212', - ) - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') + cy.chooseSavedPickupAddress(1) cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - - cy.searchAddress( - '[data-form="task"]:nth-of-type(2)', - '72 Rue Saint-Maur, 75011 Paris, France', - /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, - ) - - cy.get('#delivery_tasks_1_address_name__display').clear() - cy.get('#delivery_tasks_1_address_name__display').type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display').type( - '+33112121212', - ) - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type( - 'Jane smith', - ) + cy.chooseSavedDropoff1Address(2) cy.get('#delivery_tasks_1_weight').clear() cy.get('#delivery_tasks_1_weight').type(2.5) @@ -131,23 +97,7 @@ describe('Delivery with recurrence rule (role: admin)', () => { .click() // Pickup - - cy.searchAddress( - '[data-form="task"]:nth-of-type(1)', - '23 Avenue Claude Vellefaux, 75010 Paris, France', - /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, - ) - - cy.get('#delivery_tasks_0_address_name__display').clear() - cy.get('#delivery_tasks_0_address_name__display').type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display').type( - '+33112121212', - ) - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') + cy.chooseSavedPickupAddress(1) // set pickup time range to XX:12 - XX:27 cy.get( @@ -165,25 +115,7 @@ describe('Delivery with recurrence rule (role: admin)', () => { cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - - cy.searchAddress( - '[data-form="task"]:nth-of-type(2)', - '72 Rue Saint-Maur, 75011 Paris, France', - /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, - ) - - cy.get('#delivery_tasks_1_address_name__display').clear() - cy.get('#delivery_tasks_1_address_name__display').type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display').type( - '+33112121212', - ) - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type( - 'Jane smith', - ) + cy.chooseSavedDropoff1Address(2) // set dropoff time range to XX:24 - XX:58 cy.get( diff --git a/cypress/e2e/local-commerce/recurrence_rules/recurrence_rules.cy.js b/cypress/e2e/local-commerce/@admin/recurrence_rules/recurrence_rules.cy.js similarity index 77% rename from cypress/e2e/local-commerce/recurrence_rules/recurrence_rules.cy.js rename to cypress/e2e/local-commerce/@admin/recurrence_rules/recurrence_rules.cy.js index ad4ebe8abb..9ce7432d2c 100644 --- a/cypress/e2e/local-commerce/recurrence_rules/recurrence_rules.cy.js +++ b/cypress/e2e/local-commerce/@admin/recurrence_rules/recurrence_rules.cy.js @@ -26,46 +26,12 @@ context('Managing recurrence rules (role: admin)', () => { .click() // Pickup - - cy.searchAddress( - '[data-form="task"]:nth-of-type(1)', - '23 Avenue Claude Vellefaux, 75010 Paris, France', - /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, - ) - - cy.get('#delivery_tasks_0_address_name__display') - .clear() - cy.get('#delivery_tasks_0_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') + cy.chooseSavedPickupAddress(1) cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - - cy.searchAddress( - '[data-form="task"]:nth-of-type(2)', - '72 Rue Saint-Maur, 75011 Paris, France', - /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, - ) - - cy.get('#delivery_tasks_1_address_name__display') - .clear() - cy.get('#delivery_tasks_1_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type('Jane smith') + cy.chooseSavedDropoff1Address(2) cy.get('#delivery_tasks_1_weight').clear() cy.get('#delivery_tasks_1_weight').type(2.5) diff --git a/cypress/e2e/local-commerce/@admin/update_price.cy.js b/cypress/e2e/local-commerce/@admin/update_price.cy.js new file mode 100644 index 0000000000..b85b872ef3 --- /dev/null +++ b/cypress/e2e/local-commerce/@admin/update_price.cy.js @@ -0,0 +1,157 @@ +context('Delivery (role: admin)', () => { + beforeEach(() => { + const prefix = Cypress.env('COMMAND_PREFIX') + + let cmd = + 'bin/console coopcycle:fixtures:load -f cypress/fixtures/stores.yml --env test' + if (prefix) { + cmd = `${ prefix } ${ cmd }` + } + + cy.exec(cmd) + + cy.visit('/login') + cy.login('admin', '12345678') + }) + + it('update price calculated by pricing rules', function () { + // Create a delivery order with a price calculated by pricing rules + + cy.visit('/admin/stores') + + cy.get('[data-testid=store_Acme__list_item]') + .find('.dropdown-toggle') + .click() + + cy.get('[data-testid=store_Acme__list_item]') + .contains('Créer une livraison') + .click() + + // New delivery order page + + // Pickup + cy.chooseSavedPickupAddress(1) + + cy.get('#delivery_tasks_0_comments').type('Pickup comments') + + // Dropoff + cy.chooseSavedDropoff1Address(2) + + cy.get('#delivery_tasks_1_weight').clear() + cy.get('#delivery_tasks_1_weight').type(2.5) + + cy.get('#delivery_tasks_1_comments').type('Dropoff comments') + + cy.get('[data-tax="included"]').contains('4,99 €') + + cy.get('#delivery-submit').click() + + // list of deliveries page + cy.location('pathname', { timeout: 10000 }).should( + 'match', + /\/admin\/stores\/[0-9]+\/deliveries$/, + ) + + cy.get('[data-testid=delivery__list_item]') + .contains(/€4.99/) + .should('exist') + + cy.get('[data-testid="delivery__list_item"]') + .find('[data-testid="delivery_id"]') + .click() + + // Delivery page + cy.location('pathname', { timeout: 10000 }).should( + 'match', + /\/admin\/deliveries\/[0-9]+$/, + ) + cy.get('#delivery_arbitraryPrice').check() + cy.get('#delivery_variantName').clear() + cy.get('#delivery_variantName').type('Test product') + cy.get('#delivery_variantPrice').clear() + cy.get('#delivery_variantPrice').type('72') + cy.get('#delivery-submit').click() + + // list of deliveries page + cy.location('pathname', { timeout: 10000 }).should( + 'match', + /\/admin\/deliveries$/, + ) + cy.get('[data-testid=delivery__list_item]') + .contains(/€72.00/) + .should('exist') + }) + + it('update arbitrary price', function () { + // Create a delivery order with abritrary price + + cy.visit('/admin/stores') + + cy.get('[data-testid=store_Acme__list_item]') + .find('.dropdown-toggle') + .click() + + cy.get('[data-testid=store_Acme__list_item]') + .contains('Créer une livraison') + .click() + + // New delivery order page + + // Pickup + cy.chooseSavedPickupAddress(1) + + cy.get('#delivery_tasks_0_comments').type('Pickup comments') + + // Dropoff + cy.chooseSavedDropoff1Address(2) + + cy.get('#delivery_tasks_1_weight').clear() + cy.get('#delivery_tasks_1_weight').type(2.5) + + cy.get('#delivery_tasks_1_comments').type('Dropoff comments') + + cy.get('#delivery_arbitraryPrice').check() + cy.get('#delivery_variantName').clear() + cy.get('#delivery_variantName').type('Test product') + cy.get('#delivery_variantPrice').clear() + cy.get('#delivery_variantPrice').type('72') + + cy.get('#delivery-submit').click() + + // list of deliveries page + cy.location('pathname', { timeout: 10000 }).should( + 'match', + /\/admin\/stores\/[0-9]+\/deliveries$/, + ) + + cy.get('[data-testid=delivery__list_item]') + .contains(/€72.00/) + .should('exist') + + cy.get('[data-testid="delivery__list_item"]') + .find('[data-testid="delivery_id"]') + .click() + + // Delivery page + cy.location('pathname', { timeout: 10000 }).should( + 'match', + /\/admin\/deliveries\/[0-9]+$/, + ) + cy.get('#delivery_arbitraryPrice').check() + cy.get('#delivery_variantName').clear() + cy.get('#delivery_variantName').type('Test product') + cy.get('#delivery_variantPrice').clear() + cy.get('#delivery_variantPrice').type('34') + cy.get('#delivery-submit').click() + + // list of deliveries page + cy.location('pathname', { timeout: 10000 }).should( + 'match', + /\/admin\/deliveries$/, + ) + cy.get('[data-testid=delivery__list_item]') + .contains(/€34.00/) + .should('exist') + }) + +}) diff --git a/cypress/e2e/local-commerce/create_delivery_as_store.cy.js b/cypress/e2e/local-commerce/@store/create_delivery.cy.js similarity index 64% rename from cypress/e2e/local-commerce/create_delivery_as_store.cy.js rename to cypress/e2e/local-commerce/@store/create_delivery.cy.js index fe7206fd0e..420be1c8c4 100644 --- a/cypress/e2e/local-commerce/create_delivery_as_store.cy.js +++ b/cypress/e2e/local-commerce/@store/create_delivery.cy.js @@ -24,47 +24,28 @@ context('Delivery (role: store)', () => { // Pickup - cy.searchAddress( + cy.newPickupAddress( '[data-form="task"]:nth-of-type(1)', '23 Avenue Claude Vellefaux, 75010 Paris, France', /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, + 'Office', + '+33112121212', + 'John Doe', ) - cy.get('#delivery_tasks_0_address_name__display') - .clear() - cy.get('#delivery_tasks_0_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_0_address_telephone__display').clear() - cy.get('#delivery_tasks_0_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_0_address_contactName__display').clear() - cy.get('#delivery_tasks_0_address_contactName__display').type('John Doe') - - cy.get('#delivery_tasks_0_comments').type('Pickup comments') // Dropoff - cy.searchAddress( + cy.newDropoff1Address( '[data-form="task"]:nth-of-type(2)', '72 Rue Saint-Maur, 75011 Paris, France', /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, + 'Office', + '+33112121212', + 'Jane smith', ) - cy.get('#delivery_tasks_1_address_name__display') - .clear() - cy.get('#delivery_tasks_1_address_name__display') - .type('Office') - - cy.get('#delivery_tasks_1_address_telephone__display').clear() - cy.get('#delivery_tasks_1_address_telephone__display') - .type('+33112121212') - - cy.get('#delivery_tasks_1_address_contactName__display').clear() - cy.get('#delivery_tasks_1_address_contactName__display').type('Jane smith') - cy.get('#delivery_tasks_1_weight').clear() cy.get('#delivery_tasks_1_weight').type(2.5) @@ -88,5 +69,8 @@ context('Delivery (role: store)', () => { cy.get('[data-testid=delivery__list_item]') .contains(/72,? Rue Saint-Maur,? 75011,? Paris,? France/) .should('exist') + cy.get('[data-testid=delivery__list_item]') + .contains(/€4.99/) + .should('exist') }) }) diff --git a/cypress/e2e/local-commerce/@store/create_delivery_for_store_with_invalid_pricing.cy.js b/cypress/e2e/local-commerce/@store/create_delivery_for_store_with_invalid_pricing.cy.js new file mode 100644 index 0000000000..80d9e33d3a --- /dev/null +++ b/cypress/e2e/local-commerce/@store/create_delivery_for_store_with_invalid_pricing.cy.js @@ -0,0 +1,76 @@ +context('store with invalid pricing (role: store)', () => { + beforeEach(() => { + const prefix = Cypress.env('COMMAND_PREFIX') + + let cmd = + 'bin/console coopcycle:fixtures:load -f cypress/fixtures/stores.yml --env test' + if (prefix) { + cmd = `${prefix} ${cmd}` + } + + cy.exec(cmd) + }) + + it('create delivery for store with invalid pricing', () => { + cy.intercept('/api/routing/route/*').as('apiRoutingRoute') + + cy.visit('/login') + + cy.login('store_invalid_pricing', 'password') + + cy.location('pathname').should('eq', '/dashboard') + + cy.get('a').contains('Créer une livraison').click() + + // Pickup + + cy.newPickupAddress( + '[data-form="task"]:nth-of-type(1)', + '23 Avenue Claude Vellefaux, 75010 Paris, France', + /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, + 'Office', + '+33112121212', + 'John Doe', + ) + + cy.get('#delivery_tasks_0_comments').type('Pickup comments') + + // Dropoff + + cy.newDropoff1Address( + '[data-form="task"]:nth-of-type(2)', + '72 Rue Saint-Maur, 75011 Paris, France', + /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, + 'Office', + '+33112121212', + 'Jane smith', + ) + + cy.get('#delivery_tasks_1_weight').clear() + cy.get('#delivery_tasks_1_weight').type(2.5) + + cy.get('#delivery_tasks_1_comments').type('Dropoff comments') + + cy.wait('@apiRoutingRoute') + + cy.get('#delivery_distance') + .invoke('text') + .should('match', /[0-9.]+ Km/) + + cy.get('#delivery-submit').click() + + cy.location('pathname', { timeout: 10000 }).should( + 'match', + /\/dashboard\/stores\/[0-9]+\/deliveries$/, + ) + cy.get('[data-testid=delivery__list_item]') + .contains(/23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/) + .should('exist') + cy.get('[data-testid=delivery__list_item]') + .contains(/72,? Rue Saint-Maur,? 75011,? Paris,? France/) + .should('exist') + cy.get('[data-testid=delivery__list_item]') + .contains(/€0.00/) + .should('exist') + }) +}) diff --git a/cypress/e2e/local-commerce/@store/create_delivery_for_store_without_pricing.cy.js b/cypress/e2e/local-commerce/@store/create_delivery_for_store_without_pricing.cy.js new file mode 100644 index 0000000000..5d1a69af70 --- /dev/null +++ b/cypress/e2e/local-commerce/@store/create_delivery_for_store_without_pricing.cy.js @@ -0,0 +1,76 @@ +context('store without pricing (role: store)', () => { + beforeEach(() => { + const prefix = Cypress.env('COMMAND_PREFIX') + + let cmd = + 'bin/console coopcycle:fixtures:load -f cypress/fixtures/stores.yml --env test' + if (prefix) { + cmd = `${prefix} ${cmd}` + } + + cy.exec(cmd) + }) + + it('create delivery for store without pricing', () => { + cy.intercept('/api/routing/route/*').as('apiRoutingRoute') + + cy.visit('/login') + + cy.login('store_no_pricing', 'password') + + cy.location('pathname').should('eq', '/dashboard') + + cy.get('a').contains('Créer une livraison').click() + + // Pickup + + cy.newPickupAddress( + '[data-form="task"]:nth-of-type(1)', + '23 Avenue Claude Vellefaux, 75010 Paris, France', + /^23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/i, + 'Office', + '+33112121212', + 'John Doe', + ) + + cy.get('#delivery_tasks_0_comments').type('Pickup comments') + + // Dropoff + + cy.newDropoff1Address( + '[data-form="task"]:nth-of-type(2)', + '72 Rue Saint-Maur, 75011 Paris, France', + /^72,? Rue Saint-Maur,? 75011,? Paris,? France/i, + 'Office', + '+33112121212', + 'Jane smith', + ) + + cy.get('#delivery_tasks_1_weight').clear() + cy.get('#delivery_tasks_1_weight').type(2.5) + + cy.get('#delivery_tasks_1_comments').type('Dropoff comments') + + cy.wait('@apiRoutingRoute') + + cy.get('#delivery_distance') + .invoke('text') + .should('match', /[0-9.]+ Km/) + + cy.get('#delivery-submit').click() + + cy.location('pathname', { timeout: 10000 }).should( + 'match', + /\/dashboard\/stores\/[0-9]+\/deliveries$/, + ) + cy.get('[data-testid=delivery__list_item]') + .contains(/23,? Avenue Claude Vellefaux,? 75010,? Paris,? France/) + .should('exist') + cy.get('[data-testid=delivery__list_item]') + .contains(/72,? Rue Saint-Maur,? 75011,? Paris,? France/) + .should('exist') + cy.get('[data-testid=delivery__list_item]') + .contains(/€0.00/) + .should('exist') + }) +}) diff --git a/cypress/fixtures/stores.yml b/cypress/fixtures/stores.yml index 0b8ebabf1f..58ab548e76 100644 --- a/cypress/fixtures/stores.yml +++ b/cypress/fixtures/stores.yml @@ -3,15 +3,18 @@ include: - sylius_taxation.yml AppBundle\Entity\Base\GeoCoordinates: - geo_1: - __construct: [ "48.864577", "2.333338" ] - geo_2: - __construct: [ "48.846656", "2.369052" ] + geo_warehouse: + __construct: [ "48.8758311", "2.3675732" ] + geo_client_1: + __construct: [ "48.8638614", "2.3762269" ] AppBundle\Entity\Delivery\PricingRuleSet: pricing_rule_set_1: name: Default rules: [ '@pricing_rule_1' ] + pricing_rule_set_impossible: + name: Default + rules: [ '@pricing_rule_impossible' ] AppBundle\Entity\Delivery\PricingRule: pricing_rule_1: @@ -19,18 +22,11 @@ AppBundle\Entity\Delivery\PricingRule: price: 499 position: 1 ruleSet: '@pricing_rule_set_1' - -AppBundle\Entity\Address: - address_1: - addressLocality: 'Paris' - postalCode: '75001' - streetAddress: '272, rue Saint Honoré 75001 Paris 1er' - geo: "@geo_1" - address_2: - addressLocality: 'Paris' - postalCode: '75012' - streetAddress: '18, avenue Ledru-Rollin 75012 Paris 12ème' - geo: "@geo_1" + pricing_rule_impossible: + expression: 'distance \> 100000' + price: 499 + position: 1 + ruleSet: '@pricing_rule_set_impossible' AppBundle\Entity\TimeSlot: time_slot_1: @@ -39,6 +35,24 @@ AppBundle\Entity\TimeSlot: - 'Mo-Su 00:00-11:59' - 'Mo-Su 12:00-23:59' +AppBundle\Entity\Address: + warehouse: + name: 'Warehouse' + contactName: 'John Doe' + telephone: parse('+33112121212'))> + addressLocality: 'Paris' + postalCode: '75001' + streetAddress: '23, Avenue Claude Vellefaux, 75010 Paris, France' + geo: "@geo_warehouse" + client_1: + name: 'Office' + contactName: 'Jane smith' + telephone: parse('+33112121414'))> + addressLocality: 'Paris' + postalCode: '75009' + streetAddress: '72, Rue Saint-Maur, 75011 Paris, France' + geo: "@geo_client_1" + AppBundle\Entity\Store: store_1: name: 'Acme' @@ -46,32 +60,69 @@ AppBundle\Entity\Store: enabled: true pricingRuleSet: '@pricing_rule_set_1' timeSlot: '@time_slot_1' - createOrders: true + __calls: + - addAddress: [ "@warehouse" ] + - addAddress: [ "@client_1" ] + store_without_time_slots: name: 'Store without time slots' address: "@address_1" enabled: true pricingRuleSet: '@pricing_rule_set_1' - createOrders: true - store_do_not_create_orders: - name: 'Store with createOrders disabled' + __calls: + - addAddress: [ "@warehouse" ] + - addAddress: [ "@client_1" ] + + store_no_pricing: + name: 'Store without pricing' address: "@address_1" enabled: true - pricingRuleSet: '@pricing_rule_set_1' timeSlot: '@time_slot_1' - createOrders: false + __calls: + - addAddress: [ "@warehouse" ] + - addAddress: [ "@client_1" ] + + store_invalid_pricing: + name: 'Store with invalid pricing' + address: "@address_1" + enabled: true + pricingRuleSet: '@pricing_rule_set_impossible' + timeSlot: '@time_slot_1' + __calls: + - addAddress: [ "@warehouse" ] + - addAddress: [ "@client_1" ] AppBundle\Entity\User: - storeOwner: + store1Owner: __factory: '@Nucleos\UserBundle\Util\UserManipulator::create': - 'store_1' - 'store_1' - - 'dev@coopcycle.org' + - 'store1@coopcycle.org' - true - false roles: [ 'ROLE_USER', 'ROLE_STORE' ] stores: [ '@store_1' ] + store2Owner: + __factory: + '@Nucleos\UserBundle\Util\UserManipulator::create': + - 'store_no_pricing' + - 'password' + - 'store_no_pricing@coopcycle.org' + - true + - false + roles: [ 'ROLE_USER', 'ROLE_STORE' ] + stores: [ '@store_no_pricing' ] + store3Owner: + __factory: + '@Nucleos\UserBundle\Util\UserManipulator::create': + - 'store_invalid_pricing' + - 'password' + - 'store_invalid_pricing@coopcycle.org' + - true + - false + roles: [ 'ROLE_USER', 'ROLE_STORE' ] + stores: [ '@store_invalid_pricing' ] admin: __factory: '@Nucleos\UserBundle\Util\UserManipulator::create': diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 70f37822a9..27cdbc0c66 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -107,6 +107,56 @@ Cypress.Commands.add('searchAddress', (selector, search, match, index = 0) => { .click() }) +Cypress.Commands.add('newPickupAddress', + (addressSelector, addressSearch, addressMatch, + businessName, telephone, contactName) => { + cy.searchAddress( + addressSelector, + addressSearch, + addressMatch, + ) + + cy.get('#delivery_tasks_0_address_name__display').clear() + cy.get('#delivery_tasks_0_address_name__display').type(businessName) + + cy.get('#delivery_tasks_0_address_telephone__display').clear() + cy.get('#delivery_tasks_0_address_telephone__display').type(telephone) + + cy.get('#delivery_tasks_0_address_contactName__display').clear() + cy.get('#delivery_tasks_0_address_contactName__display').type(contactName) + }) + +Cypress.Commands.add('chooseSavedPickupAddress', + (index) => { + cy.get('#rc_select_0').click() + cy.get(`.rc-virtual-list-holder-inner > :nth-child(${ index })`).click() + }) + +Cypress.Commands.add('newDropoff1Address', + (addressSelector, addressSearch, addressMatch, + businessName, telephone, contactName) => { + cy.searchAddress( + addressSelector, + addressSearch, + addressMatch, + ) + + cy.get('#delivery_tasks_1_address_name__display').clear() + cy.get('#delivery_tasks_1_address_name__display').type(businessName) + + cy.get('#delivery_tasks_1_address_telephone__display').clear() + cy.get('#delivery_tasks_1_address_telephone__display').type(telephone) + + cy.get('#delivery_tasks_1_address_contactName__display').clear() + cy.get('#delivery_tasks_1_address_contactName__display').type(contactName) + }) + +Cypress.Commands.add('chooseSavedDropoff1Address', + (index) => { + cy.get('#rc_select_1').click() + cy.get(`.rc-virtual-list-holder-inner > :nth-child(${ index }):visible`).click() + }) + Cypress.Commands.add('enterCreditCard', () => { const date = new Date(), expDate = ('0' + (date.getMonth() + 1)).slice(-2) + diff --git a/features/deliveries.feature b/features/deliveries.feature index 7142e4bc76..f2b2a8bc70 100644 --- a/features/deliveries.feature +++ b/features/deliveries.feature @@ -79,21 +79,666 @@ Feature: Deliveries Scenario: Create delivery with implicit pickup address with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | stores.yml | + And the store with name "Acme" has an OAuth client named "Acme" + And the OAuth client with name "Acme" has an access token + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + """ + { + "pickup": { + "doneBefore": "tomorrow 13:00" + }, + "dropoff": { + "address": "48, Rue de Rivoli", + "doneBefore": "tomorrow 13:30", + "comments": "Beware of the dog\nShe bites" + } + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"@string@.startsWith('/api/deliveries')", + "@type":"http://schema.org/ParcelDelivery", + "id":@integer@, + "pickup":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone": null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "comments": "", + "weight": null, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "dropoff":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone": null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "comments": "Beware of the dog\nShe bites", + "weight":null, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "trackingUrl": @string@ + } + """ + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the OAuth client "Acme" sends a "GET" request to "/api/deliveries/1" + Then the response status code should be 200 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"/api/deliveries/1", + "@type":"http://schema.org/ParcelDelivery", + "id":1, + "pickup":@...@, + "dropoff":@...@ + } + """ + + Scenario: Create delivery with weight in dropoff task + Given the fixtures files are loaded: + | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | stores.yml | + And the store with name "Acme" has an OAuth client named "Acme" + And the OAuth client with name "Acme" has an access token + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + """ + { + "pickup": { + "doneBefore": "tomorrow 13:00" + }, + "dropoff": { + "address": "48, Rue de Rivoli", + "doneBefore": "tomorrow 13:30", + "comments": "Beware of the dog\nShe bites", + "weight": 2000 + } + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"@string@.startsWith('/api/deliveries')", + "@type":"http://schema.org/ParcelDelivery", + "id":@integer@, + "pickup":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone": null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "comments": "2.00 kg", + "weight": 2000, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "dropoff":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone": null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "comments": "Beware of the dog\nShe bites", + "weight": 2000, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "trackingUrl": @string@ + } + """ + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the OAuth client "Acme" sends a "GET" request to "/api/deliveries/1" + Then the response status code should be 200 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"/api/deliveries/1", + "@type":"http://schema.org/ParcelDelivery", + "id":1, + "pickup":@...@, + "dropoff":@...@ + } + """ + + Scenario: Create delivery with weight and packages + Given the fixtures files are loaded: + | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | stores.yml | + And the store with name "Acme" has an OAuth client named "Acme" + And the OAuth client with name "Acme" has an access token + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + """ + { + "pickup": { + "doneBefore": "tomorrow 13:00" + }, + "dropoff": { + "address": "48, Rue de Rivoli", + "doneBefore": "tomorrow 13:30", + "comments": "Beware of the dog\nShe bites", + "weight": 6000, + "packages": [ + {"type": "XL", "quantity": 2} + ] + } + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"@string@.startsWith('/api/deliveries')", + "@type":"http://schema.org/ParcelDelivery", + "id":@integer@, + "pickup":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone": null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "comments": "2 × XL\n6.00 kg", + "weight": 6000, + "packages": [ + { + "type": "XL", + "name": "XL", + "quantity": 2, + "volume_per_package": 3, + "short_code": "AB" + } + ], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "dropoff":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone": null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "comments": "Beware of the dog\nShe bites", + "weight": 6000, + "packages": [ + { + "type": "XL", + "name": "XL", + "quantity": 2, + "volume_per_package": 3, + "short_code": "AB" + } + ], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "trackingUrl": @string@ + } + """ + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the OAuth client "Acme" sends a "GET" request to "/api/deliveries/1" + Then the response status code should be 200 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"/api/deliveries/1", + "@type":"http://schema.org/ParcelDelivery", + "id":1, + "pickup":@...@, + "dropoff":@...@ + } + """ + + Scenario: Create delivery with implicit pickup address with OAuth (with before & after) + Given the fixtures files are loaded: + | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | stores.yml | + And the store with name "Acme" has an OAuth client named "Acme" + And the OAuth client with name "Acme" has an access token + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + """ + { + "pickup": { + "before": "2022-03-25 13:00" + }, + "dropoff": { + "address": "48, Rue de Rivoli", + "after": "2022-03-25 12:30", + "before": "2022-03-25 13:30" + } + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"@string@.startsWith('/api/deliveries')", + "@type":"http://schema.org/ParcelDelivery", + "id":@integer@, + "pickup":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone":null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight": null, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "dropoff":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone":null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime().startsWith(\"2022-03-25T12:30:00\")", + "before":"@string@.isDateTime().startsWith(\"2022-03-25T13:30:00\")", + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight":null, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "trackingUrl": @string@ + } + """ + + Scenario: Create delivery with pickup & dropoff with OAuth + Given the fixtures files are loaded: + | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | stores.yml | + And the store with name "Acme" has an OAuth client named "Acme" + And the OAuth client with name "Acme" has an access token + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + """ + { + "pickup": { + "address": "24, Rue de la Paix", + "doneBefore": "tomorrow 13:00" + }, + "dropoff": { + "address": "48, Rue de Rivoli", + "doneBefore": "tomorrow 13:30" + } + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"@string@.startsWith('/api/deliveries')", + "@type":"http://schema.org/ParcelDelivery", + "id":@integer@, + "pickup":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone":null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight": null, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "dropoff":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone":null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight":null, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "trackingUrl": @string@ + } + """ + + Scenario: Create delivery with pickup & dropoff as an admin + Given the fixtures files are loaded: + | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | payment_methods.yml | + | stores.yml | + Given the user "bob" is loaded: + | email | bob@coopcycle.org | + | password | 123456 | + And the user "bob" has role "ROLE_ADMIN" + Given the user "bob" is authenticated + When I add "Content-Type" header equal to "application/ld+json" + And I add "Accept" header equal to "application/ld+json" + And the user "bob" sends a "POST" request to "/api/deliveries" with body: + """ + { + "store": "/api/stores/1", + "pickup": { + "address": "24, Rue de la Paix", + "doneBefore": "tomorrow 13:00" + }, + "dropoff": { + "address": "48, Rue de Rivoli", + "doneBefore": "tomorrow 13:30" + } + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the JSON should match: + """ + { + "@context":"/api/contexts/Delivery", + "@id":"@string@.startsWith('/api/deliveries')", + "@type":"http://schema.org/ParcelDelivery", + "id":@integer@, + "pickup":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone":null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight": null, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "dropoff":{ + "@id":"@string@.startsWith('/api/tasks')", + "@type":"Task", + "id":@integer@, + "status":"TODO", + "address":{ + "@id":"@string@.startsWith('/api/addresses')", + "@type":"http://schema.org/Place", + "geo":{ + "@type":"GeoCoordinates", + "latitude":@double@, + "longitude":@double@ + }, + "streetAddress":@string@, + "telephone":null, + "name":null, + "contactName": null, + "description": null + }, + "doneAfter":"@string@.isDateTime()", + "after":"@string@.isDateTime()", + "before":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight":null, + "packages": [], + "barcode": "@array@", + "createdAt":"@string@.isDateTime()" + }, + "trackingUrl": @string@ + } + """ + + Scenario: Create delivery with pickup & dropoff as an admin in a store without pricing + Given the fixtures files are loaded: + | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | payment_methods.yml | | stores.yml | - And the store with name "Acme" has an OAuth client named "Acme" - And the OAuth client with name "Acme" has an access token + Given the user "bob" is loaded: + | email | bob@coopcycle.org | + | password | 123456 | + And the user "bob" has role "ROLE_ADMIN" + Given the user "bob" is authenticated When I add "Content-Type" header equal to "application/ld+json" And I add "Accept" header equal to "application/ld+json" - And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + And the user "bob" sends a "POST" request to "/api/deliveries" with body: """ { + "store": "/api/stores/8", "pickup": { + "address": "24, Rue de la Paix", "doneBefore": "tomorrow 13:00" }, "dropoff": { "address": "48, Rue de Rivoli", - "doneBefore": "tomorrow 13:30", - "comments": "Beware of the dog\nShe bites" + "doneBefore": "tomorrow 13:30" } } """ @@ -120,15 +765,15 @@ Feature: Deliveries "longitude":@double@ }, "streetAddress":@string@, - "telephone": null, + "telephone":null, "name":null, "contactName": null, "description": null }, "doneAfter":"@string@.isDateTime()", "after":"@string@.isDateTime()", - "doneBefore":"@string@.isDateTime()", "before":"@string@.isDateTime()", + "doneBefore":"@string@.isDateTime()", "comments": "", "weight": null, "packages": [], @@ -149,16 +794,16 @@ Feature: Deliveries "longitude":@double@ }, "streetAddress":@string@, - "telephone": null, + "telephone":null, "name":null, "contactName": null, "description": null }, "doneAfter":"@string@.isDateTime()", "after":"@string@.isDateTime()", - "doneBefore":"@string@.isDateTime()", "before":"@string@.isDateTime()", - "comments": "Beware of the dog\nShe bites", + "doneBefore":"@string@.isDateTime()", + "comments": "", "weight":null, "packages": [], "barcode": "@array@", @@ -167,42 +812,32 @@ Feature: Deliveries "trackingUrl": @string@ } """ - When I add "Content-Type" header equal to "application/ld+json" - And I add "Accept" header equal to "application/ld+json" - And the OAuth client "Acme" sends a "GET" request to "/api/deliveries/1" - Then the response status code should be 200 - And the response should be in JSON - And the JSON should match: - """ - { - "@context":"/api/contexts/Delivery", - "@id":"/api/deliveries/1", - "@type":"http://schema.org/ParcelDelivery", - "id":1, - "pickup":@...@, - "dropoff":@...@ - } - """ - Scenario: Create delivery with weight in dropoff task + Scenario: Create delivery with pickup & dropoff as an admin in a store with invalid pricing Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | payment_methods.yml | | stores.yml | - And the store with name "Acme" has an OAuth client named "Acme" - And the OAuth client with name "Acme" has an access token + Given the user "bob" is loaded: + | email | bob@coopcycle.org | + | password | 123456 | + And the user "bob" has role "ROLE_ADMIN" + Given the user "bob" is authenticated When I add "Content-Type" header equal to "application/ld+json" And I add "Accept" header equal to "application/ld+json" - And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + And the user "bob" sends a "POST" request to "/api/deliveries" with body: """ { + "store": "/api/stores/9", "pickup": { + "address": "24, Rue de la Paix", "doneBefore": "tomorrow 13:00" }, "dropoff": { "address": "48, Rue de Rivoli", - "doneBefore": "tomorrow 13:30", - "comments": "Beware of the dog\nShe bites", - "weight": 2000 + "doneBefore": "tomorrow 13:30" } } """ @@ -229,17 +864,17 @@ Feature: Deliveries "longitude":@double@ }, "streetAddress":@string@, - "telephone": null, + "telephone":null, "name":null, "contactName": null, "description": null }, "doneAfter":"@string@.isDateTime()", "after":"@string@.isDateTime()", - "doneBefore":"@string@.isDateTime()", "before":"@string@.isDateTime()", - "comments": "2.00 kg", - "weight": 2000, + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight": null, "packages": [], "barcode": "@array@", "createdAt":"@string@.isDateTime()" @@ -258,17 +893,17 @@ Feature: Deliveries "longitude":@double@ }, "streetAddress":@string@, - "telephone": null, + "telephone":null, "name":null, "contactName": null, "description": null }, "doneAfter":"@string@.isDateTime()", "after":"@string@.isDateTime()", - "doneBefore":"@string@.isDateTime()", "before":"@string@.isDateTime()", - "comments": "Beware of the dog\nShe bites", - "weight": 2000, + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight":null, "packages": [], "barcode": "@array@", "createdAt":"@string@.isDateTime()" @@ -276,45 +911,33 @@ Feature: Deliveries "trackingUrl": @string@ } """ - When I add "Content-Type" header equal to "application/ld+json" - And I add "Accept" header equal to "application/ld+json" - And the OAuth client "Acme" sends a "GET" request to "/api/deliveries/1" - Then the response status code should be 200 - And the response should be in JSON - And the JSON should match: - """ - { - "@context":"/api/contexts/Delivery", - "@id":"/api/deliveries/1", - "@type":"http://schema.org/ParcelDelivery", - "id":1, - "pickup":@...@, - "dropoff":@...@ - } - """ - Scenario: Create delivery with weight and packages + Scenario: Create delivery with pickup & dropoff as a store owner Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | payment_methods.yml | | stores.yml | - And the store with name "Acme" has an OAuth client named "Acme" - And the OAuth client with name "Acme" has an access token + Given the user "bob" is loaded: + | email | bob@coopcycle.org | + | password | 123456 | + And the user "bob" has role "ROLE_STORE" + And the store with name "Acme" belongs to user "bob" + Given the user "bob" is authenticated When I add "Content-Type" header equal to "application/ld+json" And I add "Accept" header equal to "application/ld+json" - And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + And the user "bob" sends a "POST" request to "/api/deliveries" with body: """ { + "store": "/api/stores/1", "pickup": { + "address": "24, Rue de la Paix", "doneBefore": "tomorrow 13:00" }, "dropoff": { "address": "48, Rue de Rivoli", - "doneBefore": "tomorrow 13:30", - "comments": "Beware of the dog\nShe bites", - "weight": 6000, - "packages": [ - {"type": "XL", "quantity": 2} - ] + "doneBefore": "tomorrow 13:30" } } """ @@ -341,26 +964,18 @@ Feature: Deliveries "longitude":@double@ }, "streetAddress":@string@, - "telephone": null, + "telephone":null, "name":null, "contactName": null, "description": null }, "doneAfter":"@string@.isDateTime()", "after":"@string@.isDateTime()", - "doneBefore":"@string@.isDateTime()", "before":"@string@.isDateTime()", - "comments": "2 × XL\n6.00 kg", - "weight": 6000, - "packages": [ - { - "type": "XL", - "name": "XL", - "quantity": 2, - "volume_per_package": 3, - "short_code": "AB" - } - ], + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight": null, + "packages": [], "barcode": "@array@", "createdAt":"@string@.isDateTime()" }, @@ -378,67 +993,51 @@ Feature: Deliveries "longitude":@double@ }, "streetAddress":@string@, - "telephone": null, + "telephone":null, "name":null, "contactName": null, "description": null }, "doneAfter":"@string@.isDateTime()", "after":"@string@.isDateTime()", - "doneBefore":"@string@.isDateTime()", "before":"@string@.isDateTime()", - "comments": "Beware of the dog\nShe bites", - "weight": 6000, - "packages": [ - { - "type": "XL", - "name": "XL", - "quantity": 2, - "volume_per_package": 3, - "short_code": "AB" - } - ], + "doneBefore":"@string@.isDateTime()", + "comments": "", + "weight":null, + "packages": [], "barcode": "@array@", "createdAt":"@string@.isDateTime()" }, "trackingUrl": @string@ } """ - When I add "Content-Type" header equal to "application/ld+json" - And I add "Accept" header equal to "application/ld+json" - And the OAuth client "Acme" sends a "GET" request to "/api/deliveries/1" - Then the response status code should be 200 - And the response should be in JSON - And the JSON should match: - """ - { - "@context":"/api/contexts/Delivery", - "@id":"/api/deliveries/1", - "@type":"http://schema.org/ParcelDelivery", - "id":1, - "pickup":@...@, - "dropoff":@...@ - } - """ - Scenario: Create delivery with implicit pickup address with OAuth (with before & after) + Scenario: Create delivery with pickup & dropoff as a store owner in a store without pricing Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | payment_methods.yml | | stores.yml | - And the store with name "Acme" has an OAuth client named "Acme" - And the OAuth client with name "Acme" has an access token + Given the user "bob" is loaded: + | email | bob@coopcycle.org | + | password | 123456 | + And the user "bob" has role "ROLE_STORE" + And the store with name "Acme no pricing" belongs to user "bob" + Given the user "bob" is authenticated When I add "Content-Type" header equal to "application/ld+json" And I add "Accept" header equal to "application/ld+json" - And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + And the user "bob" sends a "POST" request to "/api/deliveries" with body: """ { + "store": "/api/stores/8", "pickup": { - "before": "2022-03-25 13:00" + "address": "24, Rue de la Paix", + "doneBefore": "tomorrow 13:00" }, "dropoff": { "address": "48, Rue de Rivoli", - "after": "2022-03-25 12:30", - "before": "2022-03-25 13:30" + "doneBefore": "tomorrow 13:30" } } """ @@ -500,8 +1099,8 @@ Feature: Deliveries "description": null }, "doneAfter":"@string@.isDateTime()", - "after":"@string@.isDateTime().startsWith(\"2022-03-25T12:30:00\")", - "before":"@string@.isDateTime().startsWith(\"2022-03-25T13:30:00\")", + "after":"@string@.isDateTime()", + "before":"@string@.isDateTime()", "doneBefore":"@string@.isDateTime()", "comments": "", "weight":null, @@ -513,17 +1112,25 @@ Feature: Deliveries } """ - Scenario: Create delivery with pickup & dropoff with OAuth + Scenario: Create delivery with pickup & dropoff as a store owner in a store with invalid pricing Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | payment_methods.yml | | stores.yml | - And the store with name "Acme" has an OAuth client named "Acme" - And the OAuth client with name "Acme" has an access token + Given the user "bob" is loaded: + | email | bob@coopcycle.org | + | password | 123456 | + And the user "bob" has role "ROLE_STORE" + And the store with name "Acme invalid pricing" belongs to user "bob" + Given the user "bob" is authenticated When I add "Content-Type" header equal to "application/ld+json" And I add "Accept" header equal to "application/ld+json" - And the OAuth client "Acme" sends a "POST" request to "/api/deliveries" with body: + And the user "bob" sends a "POST" request to "/api/deliveries" with body: """ { + "store": "/api/stores/9", "pickup": { "address": "24, Rue de la Paix", "doneBefore": "tomorrow 13:00" @@ -608,6 +1215,8 @@ Feature: Deliveries Scenario: Create delivery with implicit pickup address & implicit time with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | And the store with name "Acme" has an OAuth client named "Acme" And the OAuth client with name "Acme" has an access token @@ -696,6 +1305,8 @@ Feature: Deliveries Scenario: Create delivery with details with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | And the store with name "Acme" has an OAuth client named "Acme" And the OAuth client with name "Acme" has an access token @@ -788,6 +1399,8 @@ Feature: Deliveries Scenario: Create delivery with latLng with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | And the store with name "Acme" has an OAuth client named "Acme" And the OAuth client with name "Acme" has an access token @@ -882,6 +1495,8 @@ Feature: Deliveries Scenario: Create delivery with latLng & timeSlot with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | And the store with name "Acme" has an OAuth client named "Acme" And the OAuth client with name "Acme" has an access token @@ -974,6 +1589,8 @@ Feature: Deliveries Scenario: Create delivery with latLng & timeSlot ISO 8601 with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | Given the current time is "2020-04-02 11:00:00" And the store with name "Acme" has an OAuth client named "Acme" @@ -1067,6 +1684,8 @@ Feature: Deliveries Scenario: Create delivery with existing address & timeSlot with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | And the store with name "Acme" has an OAuth client named "Acme" And the OAuth client with name "Acme" has an access token @@ -1142,6 +1761,8 @@ Feature: Deliveries Scenario: Create delivery with address.telephone = false with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | And the store with name "Acme" has an OAuth client named "Acme" And the OAuth client with name "Acme" has an access token @@ -1354,7 +1975,6 @@ Feature: Deliveries } """ - Scenario: Cancel delivery Given the fixtures files are loaded: | sylius_channels.yml | @@ -1369,6 +1989,8 @@ Feature: Deliveries Scenario: Create delivery with dates in UTC Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | Given the current time is "2022-05-05 12:00:00" And the store with name "Acme" has an OAuth client named "Acme" @@ -1482,4 +2104,3 @@ Feature: Deliveries "@type": "DeliveryImportQueue" } """ - diff --git a/features/deliveries_multi.feature b/features/deliveries_multi.feature index 9e86436c78..664d033172 100644 --- a/features/deliveries_multi.feature +++ b/features/deliveries_multi.feature @@ -3,6 +3,8 @@ Feature: Multi-step deliveries Scenario: Create delivery with pickup & dropoff with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | Given the setting "latlng" has value "48.856613,2.352222" And the store with name "Acme" has an OAuth client named "Acme" @@ -101,6 +103,8 @@ Feature: Multi-step deliveries Scenario: Create delivery with pickup & dropoff + packages with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | Given the setting "latlng" has value "48.856613,2.352222" And the store with name "Acme" has an OAuth client named "Acme" @@ -227,6 +231,8 @@ Feature: Multi-step deliveries Scenario: Create delivery with multiple pickups & 1 dropoff + packages with OAuth Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | Given the setting "latlng" has value "48.856613,2.352222" And the store with name "Acme" has an OAuth client named "Acme" @@ -349,6 +355,8 @@ Feature: Multi-step deliveries Scenario: Create delivery with multiple pickups & 1 dropoff, without time slot for pickups Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | Given the setting "latlng" has value "48.856613,2.352222" And the store with name "Acme" has an OAuth client named "Acme" diff --git a/features/fixtures/ORM/store_w_package_pricing.yml b/features/fixtures/ORM/store_w_package_pricing.yml index 135cf58058..c9164f6c78 100644 --- a/features/fixtures/ORM/store_w_package_pricing.yml +++ b/features/fixtures/ORM/store_w_package_pricing.yml @@ -84,5 +84,4 @@ AppBundle\Entity\Store: enabled: true pricingRuleSet: '@pricing_rule_set_1' timeSlot: '@time_slot_1' - createOrders: true packageSet: '@package_set_2' diff --git a/features/fixtures/ORM/stores.yml b/features/fixtures/ORM/stores.yml index 786d1de235..9a28bd9716 100644 --- a/features/fixtures/ORM/stores.yml +++ b/features/fixtures/ORM/stores.yml @@ -19,6 +19,9 @@ AppBundle\Entity\Delivery\PricingRuleSet: pricing_rule_set_4: name: Default rules: [ '@pricing_rule_4' ] + pricing_rule_set_impossible: + name: Default + rules: [ '@pricing_rule_impossible' ] AppBundle\Entity\Delivery\PricingRule: pricing_rule_1: @@ -41,6 +44,11 @@ AppBundle\Entity\Delivery\PricingRule: price: 499 position: 1 ruleSet: '@pricing_rule_set_4' + pricing_rule_impossible: + expression: 'distance \> 100000' + price: 499 + position: 1 + ruleSet: '@pricing_rule_set_impossible' AppBundle\Entity\Address: address_1: @@ -166,3 +174,23 @@ AppBundle\Entity\Store: timeSlot: '@time_slot_1' __calls: - addAddress: [ "@address_2" ] + store_8: + name: 'Acme no pricing' + address: "@address_1" + enabled: true + pricingRuleSet: null + packageSet: '@package_set_1' + timeSlot: '@time_slot_1' + timeSlots: [ "@time_slot_1", "@time_slot_2" ] + __calls: + - addAddress: [ "@address_1" ] + store_9: + name: 'Acme invalid pricing' + address: "@address_1" + enabled: true + pricingRuleSet: '@pricing_rule_set_impossible' + packageSet: '@package_set_1' + timeSlot: '@time_slot_1' + timeSlots: [ "@time_slot_1", "@time_slot_2" ] + __calls: + - addAddress: [ "@address_1" ] diff --git a/features/recurrence_rules.feature b/features/recurrence_rules.feature index 5fb53646ee..570f1c186f 100644 --- a/features/recurrence_rules.feature +++ b/features/recurrence_rules.feature @@ -343,6 +343,9 @@ Feature: Task recurrence rules Scenario: Apply recurrence rule creates delivery Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | payment_methods.yml | | users.yml | | addresses.yml | | recurrence_rules.yml | diff --git a/features/tasks.feature b/features/tasks.feature index 5ad4f8071f..48ce3ef94a 100644 --- a/features/tasks.feature +++ b/features/tasks.feature @@ -2734,7 +2734,7 @@ Feature: Tasks "@id":"/api/tasks", "@type":"hydra:Collection", "hydra:member":@array@, - "hydra:totalItems":21, + "hydra:totalItems":22, "hydra:search":{ "@*@":"@*@" } diff --git a/features/urbantz.feature b/features/urbantz.feature index b136eb8742..1e1e5fcaeb 100644 --- a/features/urbantz.feature +++ b/features/urbantz.feature @@ -3,6 +3,9 @@ Feature: Urbantz Scenario: Receive webhook for TasksAnnounced event Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | + | payment_methods.yml | | stores.yml | And the store with name "Acme" has an API key When I add "Content-Type" header equal to "application/ld+json" @@ -282,6 +285,8 @@ Feature: Urbantz Scenario: Receive webhook for TasksAnnounced event with multiple hubs Given the fixtures files are loaded: | sylius_channels.yml | + | sylius_products.yml | + | sylius_taxation.yml | | stores.yml | And the store with name "Acme" has an API key And the store with name "Acme" is associated with Urbantz hub "61289572c2b7aab94f380d76" diff --git a/phpstan.neon b/phpstan.neon index f334318927..bbfc13604b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -30,7 +30,6 @@ parameters: - '#Call to an undefined method Sylius\\Component\\Order\\Repository\\OrderRepositoryInterface::countByCustomerAndCoupon\(\)#' - '#Call to an undefined method Symfony\\Component\\DependencyInjection\\Extension\\ExtensionInterface::addSecurityListenerFactory\(\)#' - '#Strict comparison using === between null and Sylius\\Component\\Order\\Model\\OrderInterface will always evaluate to false#' - - '#Call to an undefined method [a-zA-Z\\]+::accessControl\(\)#' - '#Call to an undefined method League\\Geotools\\Distance\\DistanceInterface::flat\(\)#' - '#Call to an undefined method Sylius\\Component\\Promotion\\Model\\PromotionSubjectInterface::getRestaurant\(\)#' - '#Call to an undefined method Sylius\\Component\\(.*)RepositoryInterface::findOneBy[a-zA-Z]+\(\)#' diff --git a/src/Action/Delivery/ConfirmQuote.php b/src/Action/Delivery/ConfirmQuote.php index d23caf7c07..1814155e7d 100644 --- a/src/Action/Delivery/ConfirmQuote.php +++ b/src/Action/Delivery/ConfirmQuote.php @@ -32,7 +32,7 @@ public function __invoke(DeliveryQuote $data) { $delivery = $this->serializer->deserialize($data->getPayload(), Delivery::class, 'jsonld'); - $order = $this->orderFactory->createForDelivery($delivery, new PricingRulesBasedPrice($data->getAmount())); + $order = $this->orderFactory->createForDeliveryAndPrice($delivery, new PricingRulesBasedPrice($data->getAmount())); $store = $data->getStore(); $store->addDelivery($delivery); diff --git a/src/Action/Delivery/Create.php b/src/Action/Delivery/Create.php index 1f6becd035..8a9425a3c0 100644 --- a/src/Action/Delivery/Create.php +++ b/src/Action/Delivery/Create.php @@ -2,16 +2,30 @@ namespace AppBundle\Action\Delivery; +use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException; use AppBundle\Entity\Delivery; use AppBundle\Pricing\PricingManager; +use Symfony\Component\Validator\Validator\ValidatorInterface; class Create { - public function __construct(private PricingManager $pricingManager) - {} + public function __construct( + private readonly PricingManager $pricingManager, + private readonly ValidatorInterface $validator, + ) + { + } public function __invoke(Delivery $data) { + // The default API platform validator is called on the object returned by the Controller/Action + // but we need to validate the delivery before we can create the order + // @see ApiPlatform\Core\Validator\EventListener\ValidateListener + $errors = $this->validator->validate($data); + if (count($errors) > 0) { + throw new ValidationException($errors); + } + $this->pricingManager->createOrder($data); return $data; diff --git a/src/Action/Incident/CreateIncident.php b/src/Action/Incident/CreateIncident.php index 9f12ec646f..aa740eb3e2 100644 --- a/src/Action/Incident/CreateIncident.php +++ b/src/Action/Incident/CreateIncident.php @@ -46,7 +46,7 @@ public function findDescriptionByCode(string $code = null): ?string return self::DEFAULT_TITLE; } - public function __invoke(Incident $data, UserInterface $user, Request $request): Incident + public function __invoke(Incident $data, ?UserInterface $user, Request $request): Incident { $title = trim($data->getTitle() ?? ''); @@ -54,7 +54,9 @@ public function __invoke(Incident $data, UserInterface $user, Request $request): $data->setTitle($this->findDescriptionByCode($data->getFailureReasonCode())); } - $data->setCreatedBy($user); + if (null !== $user) { + $data->setCreatedBy($user); + } $this->em->persist($data); $this->em->flush(); diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 95abe46f0d..887f6156b3 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -2446,7 +2446,7 @@ public function newOrderAction( $variantName = $form->get('variantName')->getData(); $variantPrice = $form->get('variantPrice')->getData(); - $order = $this->createOrderForDelivery($orderFactory, $delivery, new ArbitraryPrice($variantName, $variantPrice)); + $order = $orderFactory->createForDeliveryAndPrice($delivery, new ArbitraryPrice($variantName, $variantPrice)); $order->setState(OrderInterface::STATE_ACCEPTED); @@ -2992,5 +2992,4 @@ public function cubeAction(CubeJsTokenFactory $tokenFactory) 'cube_token' => $tokenFactory->createToken(), ]); } - } diff --git a/src/Controller/EmbedController.php b/src/Controller/EmbedController.php index 6113f2d692..dec805b696 100644 --- a/src/Controller/EmbedController.php +++ b/src/Controller/EmbedController.php @@ -2,6 +2,7 @@ namespace AppBundle\Controller; +use AppBundle\Controller\Utils\AccessControlTrait; use AppBundle\Controller\Utils\DeliveryTrait; use AppBundle\Entity\Address; use AppBundle\Entity\Delivery; @@ -38,6 +39,7 @@ */ class EmbedController extends AbstractController { + use AccessControlTrait; use DeliveryTrait; public function __construct( @@ -158,15 +160,16 @@ public function deliveryStartAction($hashid, Request $request, if ($form->isSubmitted() && $form->isValid()) { - try { + $delivery = $form->getData(); + + $price = $deliveryManager->getPrice($delivery, $this->getPricingRuleSet($request)); - $delivery = $form->getData(); + if (null === $price) { + + $message = $this->translator->trans('delivery.price.error.priceCalculation', [], 'validators'); + $form->addError(new FormError($message)); - $price = $this->getDeliveryPrice( - $delivery, - $this->getPricingRuleSet($request), - $deliveryManager - ); + } else { $submission = new DeliveryFormSubmission(); $submission->setDeliveryForm($this->getDeliveryForm($request)); @@ -183,11 +186,7 @@ public function deliveryStartAction($hashid, Request $request, 'data' => $hashids->encode($submission->getId()), ]); - } catch (NoRuleMatchedException $e) { - $message = $this->translator->trans('delivery.price.error.priceCalculation', [], 'validators'); - $form->addError(new FormError($message)); } - } return $this->render('embed/delivery/start.html.twig', [ @@ -332,7 +331,7 @@ public function deliverySummaryAction($hashid, Request $request, $telephone = $form->get('telephone')->getData(); $customer = $this->findOrCreateCustomer($email, $telephone, $canonicalizer); - $order = $this->createOrderForDelivery($orderFactory, $delivery, new PricingRulesBasedPrice($price), $customer, $attach = false); + $order = $orderFactory->createForDeliveryAndPrice($delivery, new PricingRulesBasedPrice($price), $customer, false); $checkoutPayment = new CheckoutPayment($order); $paymentForm = $this->createForm(CheckoutPaymentType::class, $checkoutPayment, [ diff --git a/src/Controller/Utils/DeliveryTrait.php b/src/Controller/Utils/DeliveryTrait.php index 959efb314d..1f8ad8a5df 100644 --- a/src/Controller/Utils/DeliveryTrait.php +++ b/src/Controller/Utils/DeliveryTrait.php @@ -3,21 +3,14 @@ namespace AppBundle\Controller\Utils; use AppBundle\Entity\Delivery; -use AppBundle\Entity\Delivery\PricingRuleSet; use AppBundle\Entity\Sylius\ArbitraryPrice; -use AppBundle\Entity\Sylius\PriceInterface; -use AppBundle\Exception\Pricing\NoRuleMatchedException; -use AppBundle\Form\DeliveryType; +use AppBundle\Entity\Sylius\PricingRulesBasedPrice; +use AppBundle\Entity\Sylius\UseArbitraryPrice; use AppBundle\Form\Order\ExistingOrderType; -use AppBundle\Service\DeliveryManager; +use AppBundle\Pricing\PricingManager; use AppBundle\Service\OrderManager; -use AppBundle\Sylius\Customer\CustomerInterface; use AppBundle\Sylius\Order\OrderFactory; -use AppBundle\Sylius\Order\OrderInterface; -use AppBundle\Sylius\Order\OrderItemInterface; use Doctrine\ORM\EntityManagerInterface; -use Ramsey\Uuid\Uuid; -use Sylius\Bundle\OrderBundle\NumberAssigner\OrderNumberAssignerInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; @@ -28,28 +21,12 @@ trait DeliveryTrait */ abstract protected function getDeliveryRoutes(); - protected function createOrderForDelivery(OrderFactory $factory, Delivery $delivery, PriceInterface $price, ?CustomerInterface $customer = null, bool $attach = true): OrderInterface - { - return $factory->createForDelivery($delivery, $price, $customer, $attach); - } - - protected function getDeliveryPrice(Delivery $delivery, PricingRuleSet $pricingRuleSet, DeliveryManager $deliveryManager) - { - $price = $deliveryManager->getPrice($delivery, $pricingRuleSet); - - if (null === $price) { - throw new NoRuleMatchedException(); - } - - return (int) ($price); - } - public function deliveryAction($id, Request $request, OrderFactory $orderFactory, EntityManagerInterface $entityManager, - OrderNumberAssignerInterface $orderNumberAssigner, - OrderManager $orderManager + OrderManager $orderManager, + PricingManager $pricingManager, ) { $delivery = $entityManager @@ -60,8 +37,12 @@ public function deliveryAction($id, $routes = $request->attributes->get('routes'); + $order = $delivery->getOrder(); + $price = $order?->getDeliveryPrice(); + $form = $this->createForm(ExistingOrderType::class, $delivery, [ - 'with_arbitrary_price' => null === $delivery->getOrder(), + 'pricing_rules_based_price' => $price instanceof PricingRulesBasedPrice ? $price : null, + 'arbitrary_price' => $price instanceof ArbitraryPrice ? $price : null ]); $form->handleRequest($request); @@ -73,13 +54,21 @@ public function deliveryAction($id, $form->has('arbitraryPrice') && true === $form->get('arbitraryPrice')->getData(); if ($useArbitraryPrice) { - $this->createOrderForDeliveryWithArbitraryPrice($form, $orderFactory, $delivery, - $entityManager, $orderNumberAssigner); - } else { - $entityManager->persist($delivery); - $entityManager->flush(); + $arbitraryPrice = $this->getArbitraryPrice($form); + if (null === $order) { + // Should not happen normally, but just in case + // there is still some delivery created without an order + $order = $pricingManager->createOrder($delivery, [ + 'pricingStrategy' => new UseArbitraryPrice($arbitraryPrice), + ]); + } else { + $orderFactory->updateDeliveryPrice($order, $delivery, $arbitraryPrice); + } } + $entityManager->persist($delivery); + $entityManager->flush(); + if ($form->has('bookmark')) { $isBookmarked = true === $form->get('bookmark')->getData(); @@ -103,26 +92,23 @@ public function deliveryAction($id, ]); } - protected function createOrderForDeliveryWithArbitraryPrice( - FormInterface $form, - OrderFactory $orderFactory, - Delivery $delivery, - EntityManagerInterface $entityManager, - OrderNumberAssignerInterface $orderNumberAssigner - ) + private function getArbitraryPrice(FormInterface $form): ?ArbitraryPrice { - $variantPrice = $form->get('variantPrice')->getData(); - $variantName = $form->get('variantName')->getData(); - - $order = $this->createOrderForDelivery($orderFactory, $delivery, new ArbitraryPrice($variantName, $variantPrice)); + if (!$this->isGranted('ROLE_ADMIN')) { + return null; + } - $order->setState(OrderInterface::STATE_ACCEPTED); + if (!$form->has('arbitraryPrice')) { + return null; + } - $entityManager->persist($order); - $entityManager->flush(); + if (true !== $form->get('arbitraryPrice')->getData()) { + return null; + } - $orderNumberAssigner->assignNumber($order); + $variantPrice = $form->get('variantPrice')->getData(); + $variantName = $form->get('variantName')->getData(); - $entityManager->flush(); + return new ArbitraryPrice($variantName, $variantPrice); } } diff --git a/src/Controller/Utils/StoreTrait.php b/src/Controller/Utils/StoreTrait.php index 415deefb61..0100d1fe37 100644 --- a/src/Controller/Utils/StoreTrait.php +++ b/src/Controller/Utils/StoreTrait.php @@ -11,7 +11,6 @@ use AppBundle\Entity\Store; use AppBundle\Entity\Sylius\ArbitraryPrice; use AppBundle\Entity\Sylius\OrderRepository; -use AppBundle\Entity\Sylius\PricingRulesBasedPrice; use AppBundle\Entity\Sylius\PricingStrategy; use AppBundle\Entity\Sylius\UseArbitraryPrice; use AppBundle\Entity\Sylius\UsePricingRules; @@ -29,7 +28,6 @@ use AppBundle\Service\DeliveryManager; use AppBundle\Service\OrderManager; use AppBundle\Service\InvitationManager; -use AppBundle\Sylius\Order\OrderFactory; use AppBundle\Sylius\Order\OrderInterface; use Carbon\Carbon; use Cocur\Slugify\SlugifyInterface; @@ -45,7 +43,6 @@ use Psr\Log\LoggerInterface; use Recurr\Exception\InvalidRRule; use Recurr\Rule; -use Sylius\Bundle\OrderBundle\NumberAssigner\OrderNumberAssignerInterface; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\Extension\Core\Type\EmailType; @@ -284,11 +281,8 @@ protected function renderStoreAddressForm(Store $store, Address $address, Reques } public function newStoreDeliveryAction($id, Request $request, - DeliveryManager $deliveryManager, PricingManager $pricingManager, OrderManager $orderManager, - OrderFactory $orderFactory, - OrderNumberAssignerInterface $orderNumberAssigner, EntityManagerInterface $entityManager, TranslatorInterface $translator, LoggerInterface $logger) @@ -328,51 +322,39 @@ public function newStoreDeliveryAction($id, Request $request, $delivery = $form->getData(); - if ($arbitraryPrice = $this->getArbitraryPrice($form)) { - $this->createOrderForDeliveryWithArbitraryPrice($form, $orderFactory, $delivery, - $entityManager, $orderNumberAssigner); - - $order = $delivery->getOrder(); - $this->handleBookmark($orderManager, $form, $order); - $this->handleNewRecurrenceRule($pricingManager, $logger, $store, $form, $delivery, $order, new UseArbitraryPrice($arbitraryPrice)); - - $entityManager->flush(); + $priceForOrder = null; - return $this->redirectToRoute($routes['success'], ['id' => $id]); - - } elseif ($store->getCreateOrders()) { - - try { - - $price = $this->getDeliveryPrice($delivery, $store->getPricingRuleSet(), $deliveryManager); - $order = $this->createOrderForDelivery($orderFactory, $delivery, new PricingRulesBasedPrice($price), $this->getUser()->getCustomer()); - $entityManager->persist($order); - - $this->handleRememberAddress($store, $form); - $this->handleBookmark($orderManager, $form, $order); + if ($this->isGranted('ROLE_ADMIN') && $arbitraryPrice = $this->getArbitraryPrice($form)) { + $priceForOrder = new UseArbitraryPrice($arbitraryPrice); + } else { + $priceForOrder = new UsePricingRules(); + } - $entityManager->flush(); + $order = null; + try { + $order = $pricingManager->createOrder($delivery, [ + 'pricingStrategy' => $priceForOrder, + // Force an admin to fix the pricing rules + // maybe it would be a better UX to create an incident instead + 'throwException' => $this->isGranted('ROLE_ADMIN') + ]); - // We need to persist the order before calling onDemand, - // because an auto increment is needed to generate a number - $orderManager->onDemand($order); + } catch (NoRuleMatchedException $e) { + $message = $translator->trans('delivery.price.error.priceCalculation', [], 'validators'); + $form->addError(new FormError($message)); + } - $this->handleNewRecurrenceRule($pricingManager, $logger, $store, $form, $delivery, $order); + if (null !== $order) { - $entityManager->flush(); + $this->handleRememberAddress($store, $form); + $this->handleBookmark($orderManager, $form, $order); - return $this->redirectToRoute($routes['success'], ['id' => $id]); + $this->handleNewRecurrenceRule($pricingManager, $logger, $store, $form, $delivery, $order, $priceForOrder); - } catch (NoRuleMatchedException $e) { - $message = $translator->trans('delivery.price.error.priceCalculation', [], 'validators'); - $form->addError(new FormError($message)); + if ($this->isGranted('ROLE_ADMIN')) { + $order->setState(OrderInterface::STATE_ACCEPTED); } - } else { - - $this->handleRememberAddress($store, $form); - - $entityManager->persist($delivery); $entityManager->flush(); // TODO Add flash message @@ -436,7 +418,7 @@ public function recurrenceRuleAction($storeId, // to make sure that tasks' after/before dates are in the future $startDate = Carbon::now()->addDay()->format('Y-m-d'); $tempDelivery = $deliveryManager->createDeliveryFromRecurrenceRule($recurrenceRule, $startDate, false); - + $routes = $request->attributes->get('routes'); $arbitraryPrice = null; @@ -453,7 +435,12 @@ public function recurrenceRuleAction($storeId, $tempDelivery = $form->getData(); - $arbitraryPrice = $this->getArbitraryPrice($form); + if ($this->isGranted('ROLE_ADMIN')) { + $arbitraryPrice = $this->getArbitraryPrice($form); + } else { + $arbitraryPrice = null; + } + $recurrRule = $this->getRecurrRule($form, $logger); if (null !== $recurrRule) { @@ -535,27 +522,6 @@ private function handleNewRecurrenceRule( } } - private function getArbitraryPrice(FormInterface $form): ?ArbitraryPrice - { - if (!$this->isGranted('ROLE_ADMIN')) { - return null; - } - - if (!$form->has('arbitraryPrice')) { - return null; - } - - if (true !== $form->get('arbitraryPrice')->getData()) { - return null; - } - - $variantPrice = $form->get('variantPrice')->getData(); - $variantName = $form->get('variantName')->getData(); - - return new ArbitraryPrice($variantName, $variantPrice); - } - - private function getRecurrRule(FormInterface $form, LoggerInterface $logger): ?Rule { if (!$form->has('recurrence')) { @@ -765,7 +731,13 @@ public function storeRecurrenceRulesAction($id, Request $request, $startDate = Carbon::now()->addDay()->format('Y-m-d'); foreach ($recurrenceRules as $rule) { - $templateOrder = $pricingManager->createOrderFromRecurrenceRule($rule, $startDate, false); + $templateOrder = null; + $isInvalidPricing = false; + try { + $templateOrder = $pricingManager->createOrderFromRecurrenceRule($rule, $startDate, false, true); + } catch (NoRuleMatchedException $e) { + $isInvalidPricing = true; + } $templateDelivery = $templateOrder ? $templateOrder->getDelivery() : $deliveryManager->createDeliveryFromRecurrenceRule($rule, $startDate, false); $templateTasks = $templateDelivery ? $templateDelivery->getTasks() : $deliveryManager->createTasksFromRecurrenceRule($rule, $startDate, false); @@ -776,6 +748,7 @@ public function storeRecurrenceRulesAction($id, Request $request, 'templateTasks' => $templateTasks, 'templateDelivery' => $templateDelivery, // might be null if we cannot create a valid delivery (could be the case for some recurrence rules created from Dispatch dashboard) 'templateOrder' => $templateOrder, // might be null if we cannot calculate the price + 'isInvalidPricing' => $isInvalidPricing, 'generateOrders' => !$isLegacy && $rule->isGenerateOrders(), //in the future that will be configurable 'isLegacy' => $isLegacy, ]; @@ -903,8 +876,8 @@ protected function handleDeliveryImportForStore( MessageBusInterface $messageBus, SlugifyInterface $slugify, string $routeTo) - { - + { + /** @var UploadedFile $uploadedFile */ $uploadedFile = $form->get('file')->getData(); diff --git a/src/Entity/Incident/Incident.php b/src/Entity/Incident/Incident.php index af1ea47458..317daab38a 100644 --- a/src/Entity/Incident/Incident.php +++ b/src/Entity/Incident/Incident.php @@ -108,6 +108,7 @@ class Incident implements TaggableInterface { /** + * FIXME: allow to set $createdBy API clients (ApiApp) and integrations * @Groups({"incident"}) */ protected ?UserInterface $createdBy = null; diff --git a/src/Entity/Store.php b/src/Entity/Store.php index 1d6ef88f4c..39b79a9355 100644 --- a/src/Entity/Store.php +++ b/src/Entity/Store.php @@ -161,8 +161,6 @@ class Store extends LocalBusiness implements TaggableInterface, OrganizationAwar private $prefillPickupAddress = false; - private $createOrders = false; - /** * @ApiSubresource */ @@ -396,20 +394,6 @@ public function setPrefillPickupAddress($prefillPickupAddress) return $this; } - public function getCreateOrders() - { - return $this->createOrders; - } - /** - * @param mixed $createOrders - */ - public function setCreateOrders($createOrders) - { - $this->createOrders = $createOrders; - - return $this; - } - public function getAddresses() { return $this->addresses; diff --git a/src/Entity/Sylius/Order.php b/src/Entity/Sylius/Order.php index 5c79636900..0a5d0cb200 100644 --- a/src/Entity/Sylius/Order.php +++ b/src/Entity/Sylius/Order.php @@ -1939,4 +1939,35 @@ public function getLoopeatFormatById(int $formatId): ?array return null; } + + public function isFoodtech(): bool + { + //FIXME: combine with $this->getStoreType() implementation + return $this->hasVendor(); + } + + + public function getDeliveryPrice(): PriceInterface + { + if ($this->isFoodtech()) { + //FIXME: get the delivery price for food tech orders from Adjustments + return new PricingRulesBasedPrice(0); + } + + $deliveryItem = $this->getItems()->first(); + + if (false === $deliveryItem) { + throw new \LogicException('Order has no delivery price'); + } + + $productVariant = $deliveryItem->getVariant(); + + if (str_starts_with($productVariant->getCode(), 'CPCCL-ODDLVR')) { + // price based on the PricingRuleSet + return new PricingRulesBasedPrice($deliveryItem->getUnitPrice()); + } else { + // custom price + return new ArbitraryPrice($productVariant->getName(), $deliveryItem->getUnitPrice()); + } + } } diff --git a/src/Form/DeliveryType.php b/src/Form/DeliveryType.php index 592b157f0b..9f4d0ce148 100644 --- a/src/Form/DeliveryType.php +++ b/src/Form/DeliveryType.php @@ -59,9 +59,13 @@ public function buildForm(FormBuilderInterface $builder, array $options) $store = $delivery->getStore(); + $isNew = null === $delivery->getId(); + // other order types are foodtech and b2c deliveries (via Order form: https://docs.coopcycle.org/en/admin/deliveries/externaldisplay/) + $isStoreDeliveryOrder = $store !== null; + // When this is a new delivery, // set defaults for pickup/dropoff date - if (null === $delivery->getId() && true === $options['asap_timing']) { + if ($isNew && true === $options['asap_timing']) { $now = Carbon::now(); @@ -111,7 +115,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $isMultiDropEnabled = null !== $store ? $store->isMultiDropEnabled() : false; // customers/stores owners are not allowed to edit existing deliveries - $isEditEnabled = $this->authorizationChecker->isGranted('ROLE_DISPATCHER') || is_null($delivery->getId()); + $isEditEnabled = ($isStoreDeliveryOrder && $this->authorizationChecker->isGranted('ROLE_DISPATCHER')) || $isNew; if ($isMultiDropEnabled && $isEditEnabled) { $form->add('addTask', ButtonType::class, [ @@ -122,10 +126,26 @@ public function buildForm(FormBuilderInterface $builder, array $options) ]); } + if ($options['with_price_preview']) { + $form->add('pricePreview', HiddenType::class, [ + 'required' => false, + 'mapped' => false, + ]); + } + // Allow admins to define an arbitrary price - if (true === $options['with_arbitrary_price'] && + if (true === $options['with_arbitrary_price'] && $isStoreDeliveryOrder && $this->authorizationChecker->isGranted('ROLE_ADMIN')) { + // If the current price was calculated using pricing rules, display it as a hint + if (null !== $options['pricing_rules_based_price']) { + $form->add('pricingRulesBasedPrice', HiddenType::class, [ + 'required' => false, + 'mapped' => false, + 'data' => $options['pricing_rules_based_price']->getValue(), + ]); + } + $arbitraryPrice = $options['arbitrary_price']; $form->add('arbitraryPrice', CheckboxType::class, [ @@ -149,9 +169,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ]); } - $isDeliveryOrder = null !== $store && $store->getCreateOrders(); - - if ($options['with_bookmark'] && $isDeliveryOrder && $this->authorizationChecker->isGranted('ROLE_ADMIN')) { + if ($options['with_bookmark'] && $isStoreDeliveryOrder && $this->authorizationChecker->isGranted('ROLE_ADMIN')) { $form->add('bookmark', CheckboxType::class, [ 'label' => 'form.delivery.bookmark.label', 'mapped' => false, @@ -160,7 +178,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) ]); } - if ($options['with_recurrence'] && $isDeliveryOrder && $this->authorizationChecker->isGranted('ROLE_ADMIN')) { + if ($options['with_recurrence'] && $isStoreDeliveryOrder && $this->authorizationChecker->isGranted('ROLE_ADMIN')) { $form->add('recurrence', HiddenType::class, [ 'required' => false, 'mapped' => false, @@ -305,6 +323,8 @@ public function configureOptions(OptionsResolver $resolver) 'with_package_set' => null, 'with_remember_address' => false, 'with_address_props' => false, + 'with_price_preview' => false, + 'pricing_rules_based_price' => null, 'with_arbitrary_price' => false, 'arbitrary_price' => null, 'with_bookmark' => false, diff --git a/src/Form/Order/ExistingOrderType.php b/src/Form/Order/ExistingOrderType.php index 55be12663a..4d2a79c542 100644 --- a/src/Form/Order/ExistingOrderType.php +++ b/src/Form/Order/ExistingOrderType.php @@ -34,6 +34,7 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setDefaults([ 'with_address_props' => true, + 'with_arbitrary_price' => true, 'with_bookmark' => true, ]); } diff --git a/src/Form/Order/ExistingRecurrenceRuleType.php b/src/Form/Order/ExistingRecurrenceRuleType.php index 1f9163c52d..957650dd5e 100644 --- a/src/Form/Order/ExistingRecurrenceRuleType.php +++ b/src/Form/Order/ExistingRecurrenceRuleType.php @@ -38,6 +38,7 @@ public function configureOptions(OptionsResolver $resolver) 'with_address_props' => true, // Pre-defined time slots are supported while creating a recurrence rule, but not while modifying a recurrence rule 'use_time_slots' => false, + 'with_price_preview' => true, 'with_arbitrary_price' => true, 'with_recurrence' => true ]); diff --git a/src/Form/Order/NewOrderType.php b/src/Form/Order/NewOrderType.php index 03caf8e877..02bb2efa7d 100644 --- a/src/Form/Order/NewOrderType.php +++ b/src/Form/Order/NewOrderType.php @@ -36,6 +36,7 @@ public function configureOptions(OptionsResolver $resolver) 'with_dropoff_doorstep' => true, 'with_remember_address' => true, 'with_address_props' => true, + 'with_price_preview' => true, 'with_arbitrary_price' => true, 'with_bookmark' => true, 'with_recurrence' => true, diff --git a/src/Form/StoreType.php b/src/Form/StoreType.php index 659f773752..b28a11add1 100644 --- a/src/Form/StoreType.php +++ b/src/Form/StoreType.php @@ -36,6 +36,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'class' => PricingRuleSet::class, 'choice_label' => 'name', 'query_builder' => new OrderByNameQueryBuilder(), + 'required' => false, )) ->add('packageSet', EntityType::class, array( 'label' => 'form.store_type.package_set.label', @@ -48,10 +49,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'label' => 'form.store_type.prefill_pickup_address.label', 'required' => false, ]) - ->add('createOrders', CheckboxType::class, [ - 'label' => 'form.store_type.create_orders.label', - 'required' => false, - ]) ->add('weightRequired', CheckboxType::class, [ 'label' => 'form.store_type.weight_required.label', 'required' => false, diff --git a/src/MessageHandler/ImportDeliveriesHandler.php b/src/MessageHandler/ImportDeliveriesHandler.php index 9a2134a594..35d13933e9 100644 --- a/src/MessageHandler/ImportDeliveriesHandler.php +++ b/src/MessageHandler/ImportDeliveriesHandler.php @@ -89,7 +89,7 @@ public function __invoke(ImportDeliveries $message) try { $this->pricingManager->createOrder($delivery, [ - 'throwException' => true, + 'throwException' => true ]); } catch (NoRuleMatchedException $e) { $errorMessage = $this->translator->trans('delivery.price.error.priceCalculation', [], 'validators'); diff --git a/src/Pricing/PricingManager.php b/src/Pricing/PricingManager.php index dc4475cc14..d4a56b67ff 100644 --- a/src/Pricing/PricingManager.php +++ b/src/Pricing/PricingManager.php @@ -2,16 +2,21 @@ namespace AppBundle\Pricing; +use AppBundle\Action\Incident\CreateIncident; +use AppBundle\Action\Utils\TokenStorageTrait; use AppBundle\Entity\Delivery; +use AppBundle\Entity\Incident\Incident; use AppBundle\Entity\Store; use AppBundle\Entity\Sylius\ArbitraryPrice; use AppBundle\Entity\Sylius\Order; +use AppBundle\Entity\Sylius\PriceInterface; use AppBundle\Entity\Sylius\PricingRulesBasedPrice; use AppBundle\Entity\Sylius\UseArbitraryPrice; use AppBundle\Entity\Sylius\PricingStrategy; use AppBundle\Entity\Sylius\UsePricingRules; use AppBundle\Entity\Task; use AppBundle\Entity\Task\RecurrenceRule; +use AppBundle\Entity\User; use AppBundle\Exception\Pricing\NoRuleMatchedException; use AppBundle\Service\DeliveryManager; use AppBundle\Service\OrderManager; @@ -21,25 +26,66 @@ use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Recurr\Rule; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * FIXME: Should we merge this class into the OrderManager class? */ class PricingManager { + use TokenStorageTrait; + public function __construct( + TokenStorageInterface $tokenStorage, private readonly DeliveryManager $deliveryManager, private readonly OrderManager $orderManager, private readonly OrderFactory $orderFactory, private readonly EntityManagerInterface $entityManager, private readonly NormalizerInterface $normalizer, + private readonly RequestStack $requestStack, + private readonly CreateIncident $createIncident, + private readonly TranslatorInterface $translator, private readonly LoggerInterface $logger ) - {} + { + $this->tokenStorage = $tokenStorage; + } + + private function getDeliveryPrice(Delivery $delivery, PricingStrategy $pricingStrategy): ?PriceInterface + { + $store = $delivery->getStore(); + + if (null === $store) { + $this->logger->warning('Delivery has no store'); + return null; + } + + if ($pricingStrategy instanceof UsePricingRules) { + $price = $this->deliveryManager->getPrice($delivery, $store->getPricingRuleSet()); + + if (null === $price) { + $this->logger->warning('Price could not be calculated'); + return null; + } + + $price = (int) $price; + return new PricingRulesBasedPrice($price); + + } elseif ($pricingStrategy instanceof UseArbitraryPrice) { + return $pricingStrategy->getArbitraryPrice(); + + } else { + $this->logger->warning('Unsupported pricing config'); + return null; + } + } /** * @return OrderInterface|null + * @throws NoRuleMatchedException */ public function createOrder(Delivery $delivery, array $optionalArgs = []): ?OrderInterface { @@ -47,66 +93,72 @@ public function createOrder(Delivery $delivery, array $optionalArgs = []): ?Orde // even though it seems that it was fixed: https://github.com/sebastianbergmann/phpunit/commit/658d8decbec90c4165c0b911cf6cfeb5f6601cae $defaults = [ 'pricingStrategy' => new UsePricingRules(), - 'throwException' => false, 'persist' => true, + // If set to true, an exception will be thrown when a price cannot be calculated + // If set to false, a price of 0 will be set and an incident will be created + 'throwException' => false, ]; $optionalArgs+= $defaults; $pricingStrategy = $optionalArgs['pricingStrategy']; - $throwException = $optionalArgs['throwException']; $persist = $optionalArgs['persist']; + $throwException = $optionalArgs['throwException']; if (null === $pricingStrategy) { $pricingStrategy = new UsePricingRules(); } - $store = $delivery->getStore(); + $price = $this->getDeliveryPrice($delivery, $pricingStrategy); + $incident = null; - if (null !== $store && $store->getCreateOrders()) { + if (null === $price) { + if ($throwException) { + throw new NoRuleMatchedException(); + } - $order = null; + // otherwise; set price to 0 and create an incident + $price = new ArbitraryPrice($this->translator->trans('form.delivery.price.missing'), 0); + $incident = new Incident(); + } - if ($pricingStrategy instanceof UsePricingRules) { - $price = $this->deliveryManager->getPrice($delivery, $store->getPricingRuleSet()); + $order = $this->orderFactory->createForDeliveryAndPrice($delivery, $price); - if (null === $price) { + if ($persist) { + // We need to persist the order first, + // because an auto increment is needed to generate a number + $this->entityManager->persist($order); + $this->entityManager->flush(); - if ($throwException) { - throw new NoRuleMatchedException(); - } + $this->orderManager->onDemand($order); - $this->logger->error('Price could not be calculated'); + $this->entityManager->flush(); - return null; - } + $user = $this->getUser(); - $price = (int) $price; - $order = $this->orderFactory->createForDelivery($delivery, new PricingRulesBasedPrice($price)); + $isUserWithAccount = $user instanceof User && null !== $user->getId(); + // If it's not a user with an account, it could be an ApiApp + // ApiKey: see BearerTokenAuthenticator + // OAuth client: League\Bundle\OAuth2ServerBundle\Security\User\NullUser - } elseif ($pricingStrategy instanceof UseArbitraryPrice) { - $order = $this->orderFactory->createForDelivery($delivery, $pricingStrategy->getArbitraryPrice()); + if (null !== $incident) { + $title = $this->translator->trans('form.delivery.price.missing.incident', [ + '%number%' => $order->getNumber(), + ]); - } else { - if ($throwException) { - throw new \InvalidArgumentException('Unsupported pricing config'); + //FIXME: allow to set $createdBy API clients (ApiApp) and integrations; see Incident::createdBy + if (!$isUserWithAccount) { + $title = $title . ' (API client)'; } - } - - if ($persist) { - // We need to persist the order first, - // because an auto increment is needed to generate a number - $this->entityManager->persist($order); - $this->entityManager->flush(); - $this->orderManager->onDemand($order); + $incident->setTitle($title); + $incident->setFailureReasonCode('PRICE_REVIEW_NEEDED'); + $incident->setTask($delivery->getPickup()); - $this->entityManager->flush(); + $this->createIncident->__invoke($incident, $isUserWithAccount ? $user : null, $this->requestStack->getCurrentRequest()); } - - return $order; } - return null; + return $order; } public function duplicateOrder($store, $orderId): array | null @@ -137,21 +189,11 @@ public function duplicateOrder($store, $orderId): array | null $delivery = Delivery::createWithTasks(...$newTasks); $delivery->setStore($store); - $orderItem = $previousOrder->getItems()->first(); - $productVariant = $orderItem->getVariant(); // @phpstan-ignore method.nonObject - - $previousArbitraryPrice = null; - - if (str_starts_with($productVariant->getCode(), 'CPCCL-ODDLVR')) { - // price based on the PricingRuleSet; will be recalculated based on the latest rules - } else { - // arbitrary price - $previousArbitraryPrice = new ArbitraryPrice($productVariant->getName(), $orderItem->getUnitPrice()); - } + $previousDeliveryPrice = $previousOrder->getDeliveryPrice(); return [ 'delivery' => $delivery, - 'previousArbitraryPrice' => $previousArbitraryPrice, + 'previousArbitraryPrice' => $previousDeliveryPrice instanceof ArbitraryPrice ? $previousDeliveryPrice : null, ]; } @@ -269,7 +311,7 @@ private function setData(RecurrenceRule $recurrenceRule, Delivery $delivery, Rul $recurrenceRule->setTemplate($template); } - public function createOrderFromRecurrenceRule(Task\RecurrenceRule $recurrenceRule, string $startDate, bool $persist = true): ?OrderInterface + public function createOrderFromRecurrenceRule(Task\RecurrenceRule $recurrenceRule, string $startDate, bool $persist = true, bool $throwException = false): ?OrderInterface { $store = $recurrenceRule->getStore(); @@ -279,18 +321,21 @@ public function createOrderFromRecurrenceRule(Task\RecurrenceRule $recurrenceRul return null; } - $order = null; + $pricingStrategy = null; if ($arbitraryPriceTemplate = $recurrenceRule->getArbitraryPriceTemplate()) { - $order = $this->createOrder($delivery, [ - 'pricingStrategy' => new UseArbitraryPrice(new ArbitraryPrice($arbitraryPriceTemplate['variantName'], $arbitraryPriceTemplate['variantPrice'])), - 'persist' => $persist, - ]); + $pricingStrategy = new UseArbitraryPrice(new ArbitraryPrice($arbitraryPriceTemplate['variantName'], $arbitraryPriceTemplate['variantPrice'])); } else { - $order = $this->createOrder($delivery, [ - 'persist' => $persist, - ]); + $pricingStrategy = new UsePricingRules(); } + $order = $this->createOrder($delivery, [ + 'pricingStrategy' => $pricingStrategy, + 'persist' => $persist, + // Display an error when viewing the list of recurrence rules so an admin knows which rules need to be fixed + // When auto-generating orders, create an incident instead + 'throwException' => $throwException, + ]); + if (null !== $order) { $order->setSubscription($recurrenceRule); } diff --git a/src/Resources/config/doctrine/Store.orm.xml b/src/Resources/config/doctrine/Store.orm.xml index 8c0528b571..c8a220eb50 100644 --- a/src/Resources/config/doctrine/Store.orm.xml +++ b/src/Resources/config/doctrine/Store.orm.xml @@ -25,7 +25,6 @@ - diff --git a/src/Resources/config/failure_reasons.yml b/src/Resources/config/failure_reasons.yml index da7554f010..48104e4676 100644 --- a/src/Resources/config/failure_reasons.yml +++ b/src/Resources/config/failure_reasons.yml @@ -21,6 +21,7 @@ failure_reasons: - { code: "PACKAGING_ISSUE", description: "delivery.failure_reason.default.packaging_problems" } - { code: "HOLIDAY", description: "delivery.failure_reason.default.holiday_closure" } - { code: "ADDRESS_REVIEW_NEEDED", description: "delivery.failure_reason.default.address_review_needed" } + - { code: "PRICE_REVIEW_NEEDED", description: "delivery.failure_reason.default.price_review_needed" } dbschenker: - { option: "state", code: "AAR", description: "delivery.failure_reason.transporter.state.aar" } - { option: "state", code: "RAQ", description: "delivery.failure_reason.transporter.state.raq" } diff --git a/src/Service/DeliveryManager.php b/src/Service/DeliveryManager.php index 7b1e37044c..117b863eaf 100644 --- a/src/Service/DeliveryManager.php +++ b/src/Service/DeliveryManager.php @@ -34,11 +34,17 @@ public function __construct( ) {} - public function getPrice(Delivery $delivery, PricingRuleSet $ruleSet) + public function getPrice(Delivery $delivery, ?PricingRuleSet $ruleSet): ?int { + // if no Pricing Rules are defined, the default rule is to set the price to 0 + if (null === $ruleSet) { + return 0; + } + $visitor = new PriceCalculationVisitor($ruleSet, $this->expressionLanguage, $this->logger); $visitor->visitDelivery($delivery); + // if the Pricing Rules are configured but none of them matched, the price is null return $visitor->getPrice(); } diff --git a/src/Sylius/Order/OrderFactory.php b/src/Sylius/Order/OrderFactory.php index 8b18fcff25..5bdab1a215 100644 --- a/src/Sylius/Order/OrderFactory.php +++ b/src/Sylius/Order/OrderFactory.php @@ -9,64 +9,26 @@ use AppBundle\Entity\Sylius\PriceInterface; use AppBundle\Sylius\Customer\CustomerInterface; use AppBundle\Sylius\Product\ProductVariantFactory; +use Psr\Log\LoggerInterface; use Ramsey\Uuid\Uuid; use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Resource\Factory\FactoryInterface; -use Sylius\Component\Taxation\Calculator\CalculatorInterface; -use Sylius\Component\Product\Factory\ProductVariantFactoryInterface; use Sylius\Component\Order\Modifier\OrderModifierInterface; use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface; use Webmozart\Assert\Assert; class OrderFactory implements FactoryInterface { - /** - * @var FactoryInterface - */ - private $factory; - - /** - * @var ChannelContextInterface - */ - private $channelContext; - - /** - * @var FactoryInterface $orderItemFactory - */ - private $orderItemFactory; - - /** - * @var ProductVariantFactoryInterface $productVariantFactory - */ - private $productVariantFactory; - - /** - * @var OrderItemQuantityModifierInterface $orderItemQuantityModifier - */ - private $orderItemQuantityModifier; - - /** - * @var OrderModifierInterface $orderModifier - */ - private $orderModifier; - - /** - * @param FactoryInterface $factory - */ public function __construct( - FactoryInterface $factory, - ChannelContextInterface $channelContext, - FactoryInterface $orderItemFactory, - ProductVariantFactoryInterface $productVariantFactory, - OrderItemQuantityModifierInterface $orderItemQuantityModifier, - OrderModifierInterface $orderModifier) + private readonly FactoryInterface $factory, + private readonly ChannelContextInterface $channelContext, + private readonly FactoryInterface $orderItemFactory, + private readonly ProductVariantFactory $productVariantFactory, + private readonly OrderItemQuantityModifierInterface $orderItemQuantityModifier, + private readonly OrderModifierInterface $orderModifier, + private readonly LoggerInterface $logger + ) { - $this->factory = $factory; - $this->channelContext = $channelContext; - $this->orderItemFactory = $orderItemFactory; - $this->productVariantFactory = $productVariantFactory; - $this->orderItemQuantityModifier = $orderItemQuantityModifier; - $this->orderModifier = $orderModifier; } /** @@ -92,7 +54,7 @@ public function createForRestaurant(LocalBusiness $restaurant) return $order; } - public function createForDelivery(Delivery $delivery, PriceInterface $price, ?CustomerInterface $customer = null, $attach = true): OrderInterface + public function createForDeliveryAndPrice(Delivery $delivery, PriceInterface $price, ?CustomerInterface $customer = null, $attach = true): OrderInterface { Assert::isInstanceOf($this->productVariantFactory, ProductVariantFactory::class); @@ -120,6 +82,13 @@ public function createForDelivery(Delivery $delivery, PriceInterface $price, ?Cu $order->setCustomer($customer); } + $this->setDeliveryPrice($order, $delivery, $price); + + return $order; + } + + private function setDeliveryPrice(OrderInterface $order, Delivery $delivery, PriceInterface $price) + { $variant = $this->productVariantFactory->createForDelivery($delivery, $price->getValue()); $orderItem = $this->orderItemFactory->createNew(); @@ -135,7 +104,24 @@ public function createForDelivery(Delivery $delivery, PriceInterface $price, ?Cu $this->orderItemQuantityModifier->modify($orderItem, 1); $this->orderModifier->addToOrder($order, $orderItem); + } - return $order; + public function updateDeliveryPrice(OrderInterface $order, Delivery $delivery, PriceInterface $price) + { + if ($order->isFoodtech()) { + $this->logger->info('Price update is not supported for foodtech orders'); + return; + } + + $deliveryItem = $order->getItems()->first(); + + if (false === $deliveryItem) { + $this->logger->info('No delivery item found in order'); + } + + // remove the previous price + $this->orderModifier->removeFromOrder($order, $deliveryItem); + + $this->setDeliveryPrice($order, $delivery, $price); } } diff --git a/src/Sylius/Order/OrderInterface.php b/src/Sylius/Order/OrderInterface.php index 9e6e98c122..503e9048ac 100644 --- a/src/Sylius/Order/OrderInterface.php +++ b/src/Sylius/Order/OrderInterface.php @@ -9,6 +9,7 @@ use AppBundle\Entity\Hub; use AppBundle\Entity\LocalBusiness; use AppBundle\Entity\Sylius\OrderEvent; +use AppBundle\Entity\Sylius\PriceInterface; use AppBundle\Entity\Vendor; use AppBundle\LoopEat\LoopeatAwareInterface; use AppBundle\LoopEat\OAuthCredentialsInterface as LoopeatOAuthCredentialsInterface; @@ -265,4 +266,8 @@ public function getBookmarks(): Collection; * @return PaymentInterface|null */ public function getLastPaymentByMethod(string $method, ?string $state = null): ?PaymentInterface; + + public function isFoodtech(): bool; + + public function getDeliveryPrice(): PriceInterface; } diff --git a/src/Sylius/Product/ProductVariantFactory.php b/src/Sylius/Product/ProductVariantFactory.php index d0dab02c57..b83c715155 100644 --- a/src/Sylius/Product/ProductVariantFactory.php +++ b/src/Sylius/Product/ProductVariantFactory.php @@ -9,7 +9,6 @@ use Sylius\Component\Product\Factory\ProductVariantFactoryInterface; use Sylius\Component\Product\Repository\ProductRepositoryInterface; use Sylius\Component\Product\Repository\ProductVariantRepositoryInterface; -use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Taxation\Repository\TaxCategoryRepositoryInterface; use Symfony\Contracts\Translation\TranslatorInterface; diff --git a/templates/_partials/recurrence_rule/list.html.twig b/templates/_partials/recurrence_rule/list.html.twig index affb9962b7..136f970351 100644 --- a/templates/_partials/recurrence_rule/list.html.twig +++ b/templates/_partials/recurrence_rule/list.html.twig @@ -60,6 +60,12 @@ class="text-monospace">{{ templateOrder.total|price_format }} {% endif %} + {% if item.isInvalidPricing %} +
+
{% trans %}recurrence_rule.price.missing{% endtrans %}
+
+ {% endif %} diff --git a/templates/admin/pricing_rule_set.html.twig b/templates/admin/pricing_rule_set.html.twig index 346830092c..e846d24f6e 100644 --- a/templates/admin/pricing_rule_set.html.twig +++ b/templates/admin/pricing_rule_set.html.twig @@ -1,5 +1,14 @@ {% extends "deliveries.html.twig" %} +{% block prepare_title %} + {% set pricing_rule_set = form.vars.value %} + {% if pricing_rule_set.id %} + {{ add_title_prefix('basics.editing'|trans ~ ': ' ~ pricing_rule_set.name) }} + {% else %} + {{ add_title_prefix('pricing_rule_set.new'|trans) }} + {% endif %} +{% endblock %} + {% form_theme form 'form/pricing_rule_set.html.twig' %} {% block breadcrumb %} diff --git a/templates/delivery/form.html.twig b/templates/delivery/form.html.twig index b8395d3fbe..b8ae59f252 100644 --- a/templates/delivery/form.html.twig +++ b/templates/delivery/form.html.twig @@ -15,6 +15,7 @@ {% block content %} {% set is_new = delivery.id is null %} + {% set is_store_delivery_order = delivery.store is not empty %} {{ form_start(form, { attr: { 'data-store': delivery.store is not empty ? delivery.store|get_iri_from_item : null, @@ -52,6 +53,7 @@ }) }}> {% endif %} +
@@ -63,6 +65,59 @@
+ + {% if form.pricePreview is defined %} +
+
+ +
+ {% trans %}form.delivery.price.label{% endtrans %} + + + {{ 0|price_format }} + {{ 'basics.tax_incl'|trans }} + + + {{ 0|price_format }} + {{ 'basics.tax_excl'|trans }} + + +
+ +
+ +
+ + {% if debug_pricing is defined and debug_pricing %} + {% set pricing_rules = delivery.store.pricingRuleSet.rules %} +
    + {% for pricing_rule in pricing_rules %} +
  • + {{ pricing_rule.expression }} + + + +
  • + {% endfor %} +
+ {% endif %} + +
+ {% endif %} + + {% if form.pricingRulesBasedPrice is defined %} +
+
+ {% trans %}form.delivery.price.label{% endtrans %} + + + {{ form.pricingRulesBasedPrice.vars.data|price_format }} + {{ 'basics.tax_incl'|trans }} + + +
+ {% endif %} + {% if form.arbitraryPrice is defined %}
{{ form_row(form.arbitraryPrice) }} @@ -91,52 +146,15 @@
- {% if delivery.id is empty and delivery.store is not empty and delivery.store.createOrders %} -
- -
- {% trans %}form.delivery.price.label{% endtrans %} - - - {{ 0|price_format }} - {{ 'basics.tax_incl'|trans }} - - - {{ 0|price_format }} - {{ 'basics.tax_excl'|trans }} - - -
- -
- -
- - {% if debug_pricing is defined and debug_pricing %} - {% set pricing_rules = delivery.store.pricingRuleSet.rules %} -
    - {% for pricing_rule in pricing_rules %} -
  • - {{ pricing_rule.expression }} - - - -
  • - {% endfor %} -
- {% endif %} - -
- {% endif %}
- {% if is_new or is_granted('ROLE_DISPATCHER') %} + {% if is_new or (is_store_delivery_order and is_granted('ROLE_DISPATCHER')) %} {% endif %} - {% if is_granted('ROLE_ADMIN') and not is_new and delivery.store is not empty and delivery.order is not empty %} + {% if is_granted('ROLE_DISPATCHER') and not is_new and is_store_delivery_order and delivery.order is not empty %} diff --git a/templates/store/form/_partials/settings.html.twig b/templates/store/form/_partials/settings.html.twig index 1d24b46bca..d8c4cf6361 100644 --- a/templates/store/form/_partials/settings.html.twig +++ b/templates/store/form/_partials/settings.html.twig @@ -1,64 +1,60 @@ -{% if form.pricingRuleSet is defined or form.prefillPickupAddress is defined or form.createOrders is defined %} - {% if form.pricingRuleSet is defined and form.pricingRuleSet.vars.choices|length == 0 %} -
{% trans with {'%editPricingRule%':admin_deliveries_pricing} %}restaurant.form.noPricingRule{% endtrans %}
+{% if form.pricingRuleSet is defined %} + {% if form.pricingRuleSet.vars.choices|length == 0 %} +
{% trans with {'%editPricingRule%':admin_deliveries_pricing} %}restaurant.form.noPricingRule{% endtrans %}
{% else %} - {% if form.pricingRuleSet is defined %} - {{ form_row(form.pricingRuleSet) }} - {% endif %} - {% if form.prefillPickupAddress is defined %} - {{ form_row(form.prefillPickupAddress) }} - {% endif %} - {% if form.createOrders is defined %} - {{ form_row(form.createOrders) }} - {% endif %} - {% if form.weightRequired is defined %} - {{ form_row(form.weightRequired) }} - {% endif %} - {% if form.packagesRequired is defined %} - {{ form_row(form.packagesRequired) }} - {% endif %} - {% if form.multiDropEnabled is defined %} - {{ form_row(form.multiDropEnabled) }} - {% endif %} - {% if form.checkExpression is defined %} - {{ form_row(form.checkExpression) }} + {{ form_row(form.pricingRuleSet) }} + {% if form.pricingRuleSet.vars.data is null %} +
{% trans %}form.store_type.pricing_rule_set.warning{% endtrans %}
{% endif %} {% endif %} -
+{% endif %} + +{% if form.prefillPickupAddress is defined %} + {{ form_row(form.prefillPickupAddress) }} +{% endif %} +{% if form.weightRequired is defined %} + {{ form_row(form.weightRequired) }} +{% endif %} +{% if form.packagesRequired is defined %} + {{ form_row(form.packagesRequired) }} +{% endif %} +{% if form.multiDropEnabled is defined %} + {{ form_row(form.multiDropEnabled) }} +{% endif %} +{% if form.checkExpression is defined %} + {{ form_row(form.checkExpression) }} {% endif %} {% if form.transporter is defined %} - {{ form_row(form.transporter) }}
+ {{ form_row(form.transporter) }} {% endif %} {% if form.vars.data.id is not null %} +
-
{% endif %} {% if form.packageSet is defined %} - {{ form_row(form.packageSet) }}
+ {{ form_row(form.packageSet) }} {% endif %} {% if form.tags is defined %} +
{{ form_row(form.tags) }} {% endif %} -
+ {% if form.failureReasonSet is defined %} +
{{ form_row(form.failureReasonSet) }} {% endif %} diff --git a/templates/store/list.html.twig b/templates/store/list.html.twig index ff8b09b142..b57bc50297 100644 --- a/templates/store/list.html.twig +++ b/templates/store/list.html.twig @@ -18,22 +18,19 @@
{{ store.pricingRuleSet.name }}
+ {% else %} + {{ 'form.store_type.pricing_rule_set.warning.list'|trans }} {% endif %} {% if store.packageSet is not empty %} {{ 'form.store_type.package_set.label'|trans }} {{ store.packageSet.name }} {% endif %} - {% if store.prefillPickupAddress or store.createOrders or store.timeSlot is not empty or store.packageSet is not empty %} + {% if store.prefillPickupAddress or store.timeSlot is not empty %}
    {% if store.prefillPickupAddress %}
  • {{ 'form.store_type.prefill_pickup_address.label'|trans }}
  • {% endif %} - {% if store.createOrders %} -
  • - {{ 'form.store_type.create_orders.label'|trans }} -
  • - {% endif %} {% if store.timeSlot is not empty %}
  • {{ 'form.store_type.time_slot.label'|trans }} {{ store.timeSlot.name }} diff --git a/tests/Behat/FeatureContext.php b/tests/Behat/FeatureContext.php index 5d8307a847..94a17e7635 100755 --- a/tests/Behat/FeatureContext.php +++ b/tests/Behat/FeatureContext.php @@ -1242,7 +1242,6 @@ public function theStoreWithNameHasACheckExpressionForZone($storeName, $zoneName public function theStoreWithNameHasOrderCreationEnabled($storeName) { $store = $this->doctrine->getRepository(Store::class)->findOneByName($storeName); - $store->setCreateOrders(true); $this->doctrine->getManagerForClass(Store::class)->flush(); } diff --git a/translations/messages.en.yml b/translations/messages.en.yml index 7eb9ca717a..346b61bcca 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -431,6 +431,10 @@ checkout.breadcrumb.payment: Payment checkout.breadcrumb.confirm: Confirmation form.geojson_upload.file: File form.store_type.pricing_rule_set.label: Pricing +form.store_type.pricing_rule_set.warning: Without pricing, + the delivery price cannot be calculated and tracked for invoicing. + Also, contribution based on the order amount will not be available. +form.store_type.pricing_rule_set.warning.list: Pricing is not configured form.store_type.prefill_pickup_address.label: Pre-fill automatically pickup address form.store_type.create_orders.label: Create orders form.delivery.vehicle.VEHICLE_BIKE: Bike @@ -461,6 +465,8 @@ form.delivery.duration.label: Duration form.delivery.store.label: Store form.delivery.store.placeholder: Search a store… form.delivery.price.label: Price +form.delivery.price.missing: Contact us for more details +form.delivery.price.missing.incident: 'Order #%number%: The delivery price could not be calculated. Please enter it manually and check the pricing.' form.delivery.to_be_confirmed.warning: You must confirm the order form.delivery.has_order.info: This delivery is linked to an order form.delivery.package.label: Package @@ -506,6 +512,7 @@ form.address.company.function.label: Contact person position form.address.company.collaborator_number.label: Number of employees form.address.company.meal_estimate.label: Estimated number of meals (per day) form.company.make_the_request.label: Send request +pricing_rule_set.new: New pricing form.pricing_rule_set.name.label: Name form.pricing_rule_set.rules.label: Rules form.pricing_rule_set.strategy.label: Method of calculation @@ -872,6 +879,7 @@ stores.recurrence_rules.empty_message: When a recurring order is created, the re recurrence_rules.table.repeat: Repeat recurrence_rules.table.generate_orders: Generate tasks/orders recurrence_rules.table.created_at: Rule created +recurrence_rule.price.missing: The delivery price could not be calculated. Please check the pricing. recurrence_rule.legacy: Use dispatch dashboard to remove this rule meta.title: Delivery platform for worker-owned business admin.settings.missing_mandatory_settings: "Mandatory settings are missing.\nGo to\ diff --git a/translations/messages.es.yml b/translations/messages.es.yml index 6844a27688..afbe62bf16 100644 --- a/translations/messages.es.yml +++ b/translations/messages.es.yml @@ -299,6 +299,10 @@ form.store_type.create_orders.label: Crear pedidos form.delivery.vehicle.VEHICLE_BIKE: Bicicleta form.delivery.vehicle.VEHICLE_CARGO_BIKE: Bicicleta de carga form.store_type.pricing_rule_set.placeholder: Escoge una tarifa +form.store_type.pricing_rule_set.warning: Sin tarificación, + el precio de entrega no se puede calcular ni rastrear para la facturación. + Además, la contribución basada en el monto del pedido no estará disponible. +form.store_type.pricing_rule_set.warning.list: La tarificación no está configurada form.store_type.setAsDefault.label: Establecer como dirección predeterminada form.store_type.defaultAddress.label: Dirección predeterminada form.delivery.vehicle.placeholder: Escoge un vehículo @@ -314,6 +318,8 @@ form.delivery.duration.label: Duración form.delivery.store.label: Tienda form.delivery.store.placeholder: Busca una tienda… form.delivery.price.label: Precio +form.delivery.price.missing: Contáctenos para más detalles +form.delivery.price.missing.incident: "Pedido #%number%: No se pudo calcular el precio de entrega. Por favor, ingréselo manualmente y verifique la tarificación." form.order.accept.label: Aceptar form.order.accept.help: Una vez que haya aceptado el pedido, el cliente será notificado por correo electrónico. @@ -334,6 +340,7 @@ form.address.addressLocality.label: Ciudad form.address.postalCode.label: Código postal form.address.description.label: Instrucciones para la entrega form.address.description.placeholder: 2ª puerta a la izquierda… +pricing_rule_set.new: Nueva tarifa form.pricing_rule_set.name.label: Nombre form.pricing_rule_set.rules.label: Reglas form.pricing_rule.price.label: Precio @@ -551,6 +558,7 @@ recurrence_rules.table.repeat: Repetir recurrence_rules.table.generate_orders: Generar tareas/pedidos recurrence_rules.table.created_at: Regla creada recurrence_rule.legacy: Utilice el panel de despacho para eliminar esta regla +recurrence_rule.price.missing: No se pudo calcular el precio de entrega. Por favor, verifique la tarificación. meta.title: Plataforma de logística para cooperativas admin.settings.missing_mandatory_settings: "Algunos ajustes obligatorios no han sido\ \ configurados.\nVes a la página de ajustes para\ diff --git a/translations/messages.fr.yml b/translations/messages.fr.yml index da25687c78..372a5e03da 100644 --- a/translations/messages.fr.yml +++ b/translations/messages.fr.yml @@ -362,6 +362,10 @@ checkout.breadcrumb.payment: Paiement checkout.breadcrumb.confirm: Confirmation form.geojson_upload.file: Fichier form.store_type.pricing_rule_set.label: Tarification +form.store_type.pricing_rule_set.warning: Sans tarification, + le prix de livraison ne peut pas être calculé et suivi pour la facturation. + De plus, la contribution basée sur le montant de la commande ne sera pas disponible. +form.store_type.pricing_rule_set.warning.list: La tarification n'est pas configurée form.store_type.prefill_pickup_address.label: Remplir automatiquement l'adresse de retrait form.store_type.create_orders.label: Créer des commandes @@ -389,6 +393,8 @@ form.delivery.store.label: Magasin form.delivery.store.placeholder: Rechercher un magasin… form.delivery.confirm.label: Confirmer form.delivery.price.label: Prix +form.delivery.price.missing: Contactez-nous pour plus de détails +form.delivery.price.missing.incident: "Commande #%number%: Le prix de livraison n'a pas pu être calculé. Veuillez l'entrer manuellement et vérifier la tarification." form.delivery.to_be_confirmed.warning: Vous devez confirmer la commande form.delivery.has_order.info: Cette livraison est liée à une commande form.delivery.view_order: Voir la commande @@ -428,6 +434,7 @@ form.address.company.function.label: Poste form.address.company.collaborator_number.label: Nombre de collaborateur-ices form.address.company.meal_estimate.label: Estimation du nombre de repas (par jour) form.company.make_the_request.label: Faire la demande +pricing_rule_set.new: Nouvelle tarification form.pricing_rule_set.name.label: Nom form.pricing_rule_set.rules.label: Règles form.pricing_rule.price.label: Prix @@ -705,6 +712,7 @@ recurrence_rules.table.repeat: Répéter recurrence_rules.table.generate_orders: Générer des tâches/commandes automatiquement recurrence_rules.table.created_at: Règle créée recurrence_rule.legacy: Supprimer cette règle dans le panneau de dispatch +recurrence_rule.price.missing: Le prix de livraison n'a pas pu être calculé. Veuillez vérifier la tarification. suggest.submitted.title: Merci de nous avoir suggéré ce restaurant ! meta.title: Plateforme de livraison pour les sociétés coopératives admin.settings.missing_mandatory_settings: "Des paramètres obligatoires sont manquants.\n\