diff --git a/api/config_sample.php b/api/config_sample.php index 164e4735b..b2e40c6e4 100644 --- a/api/config_sample.php +++ b/api/config_sample.php @@ -261,6 +261,7 @@ # Shipping service details $use_shipping_service = null; + $use_shipping_service_nde = False; $use_shipping_service_incoming_shipments = null; $use_shipping_service_redirect = null; $use_shipping_service_redirect_incoming_shipments = null; diff --git a/api/index.php b/api/index.php index 286e85749..f4116e816 100644 --- a/api/index.php +++ b/api/index.php @@ -72,6 +72,7 @@ function setupApplication($mode): Slim $visit_persist_storage_dir_segment, $dhl_enable, $scale_grid, $scale_grid_end_date, $preset_proposal, $timezone, $valid_components, $enabled_container_types, $synchweb_version, $redirects, $shipping_service_app_url, $use_shipping_service_redirect, $use_shipping_service_redirect_incoming_shipments, + $use_shipping_service_nde, $dials_rest_url_rings, $closed_proposal_link, $ccp4_cloud_upload_url, $only_staff_can_assign, $industrial_prop_codes, $upstream_reprocessing_pipelines, $downstream_reprocessing_pipelines, $prop_codes_data_deleted, $container_types_with_parents, $bl_capacity; @@ -99,8 +100,9 @@ function setupApplication($mode): Slim 'enabled_container_types' => $enabled_container_types, 'synchweb_version' => $synchweb_version, 'redirects' => $redirects, - 'shipping_service_app_url' => $use_shipping_service_redirect || $use_shipping_service_redirect_incoming_shipments ? $shipping_service_app_url : null, + 'shipping_service_app_url' => $use_shipping_service_redirect ? $shipping_service_app_url : null, 'shipping_service_app_url_incoming' => $use_shipping_service_redirect_incoming_shipments ? $shipping_service_app_url : null, + 'use_shipping_service_nde' => $use_shipping_service_nde, 'dials_rest_url_rings' => $dials_rest_url_rings, 'closed_proposal_link' => $closed_proposal_link, 'ccp4_cloud_upload_url' => $ccp4_cloud_upload_url, diff --git a/api/src/Page/Shipment.php b/api/src/Page/Shipment.php index c62014c69..63131f168 100644 --- a/api/src/Page/Shipment.php +++ b/api/src/Page/Shipment.php @@ -209,7 +209,7 @@ class Shipment extends Page array('/dewars/transfer', 'post', '_transfer_dewar'), array('/dewars/dispatch', 'post', '_dispatch_dewar'), - array('/dewars/confirmdispatch/did/:did/token/:TOKEN', 'post', '_dispatch_dewar_confirmation'), + array('/dewars/confirmdispatch/did/:did/token/:TOKEN', 'post', '_outgoing_shipment_confirmation'), array('/dewars/confirmpickup/sid/:sid/token/:TOKEN', 'post', '_incoming_shipment_confirmation'), array('/dewars/tracking(/:DEWARID)', 'get', '_get_dewar_tracking'), @@ -1180,11 +1180,11 @@ function _dispatch_dewar_in_shipping_service($dispatch_info, $dewar) function _dispatch_dewar() { global $facility_country; - global $facility_courier_countries; + global $facility_courier_countries, $facility_courier_countries_nde; global $dispatch_email; global $dispatch_email_regex; global $dispatch_email_intl; - global $use_shipping_service; + global $use_shipping_service, $use_shipping_service_nde; global $shipping_service_links_in_emails; global $use_shipping_service_redirect; global $shipping_service_app_url; @@ -1266,7 +1266,10 @@ function _dispatch_dewar() $data = $this->args; $data['TERMSACCEPTED'] = $terms_accepted; - if (Utils::getValueOrDefault($use_shipping_service) && in_array($country, $facility_courier_countries)) { + $domestic = in_array($country, $facility_courier_countries); + $nde = $use_shipping_service_nde && in_array($country, $facility_courier_countries_nde); + + if (Utils::getValueOrDefault($use_shipping_service) && ($domestic || $nde)) { if ($terms_accepted) { if (Utils::getValueOrDefault($use_shipping_service_redirect)) { try { @@ -1300,7 +1303,7 @@ function _dispatch_dewar() # Prepare e-mail response for dispatch request $use_dispatch_lite_template = ( Utils::getValueOrDefault($use_shipping_service) - && in_array($country, $facility_courier_countries) + && ($domestic || $nde) && $terms_accepted && Utils::getValueOrDefault($use_shipping_service_redirect) ); @@ -1389,6 +1392,23 @@ function _dispatch_dewar() } } + function _outgoing_shipment_confirmation() + { + if (!$this->has_arg('did')) + $this->_error('No dewar specified'); + if (!$this->has_arg('TOKEN')) + $this->_error('No token specified'); + if (!$this->has_arg('status')) + $this->_error('No status specified'); + + if ($this->arg('status') === 'CREATED') { + $this->_cancel_dispatch_dewar_confirmation(); + } else if ($this->arg('status') === 'BOOKED' || $this->arg('status') === 'PENDING') { + $this->_dispatch_dewar_confirmation(); + } else { + $this->_error('Invalid status'); + } + } function _dispatch_dewar_confirmation() { @@ -1396,9 +1416,6 @@ function _dispatch_dewar_confirmation() global $dispatch_email_regex; global $shipping_service_app_url; - if (!$this->has_arg('did')) - $this->_error('No dewar specified'); - # Check token against dewar given $dew = $this->db->pq( "SELECT d.dewarid, d.barcode, d.storagelocation, d.dewarstatus, d.externalShippingIdFromSynchrotron, d.facilitycode, d.trackingNumberFromSynchrotron, @@ -1420,7 +1437,18 @@ function _dispatch_dewar_confirmation() $this->_error('Incorrect token'); } - # Prepare e-mail to stores + if ($this->arg('status') === 'BOOKED') { + if (!$this->has_arg('tracking_number')) + $this->_error('No tracking number specified'); + $tracking = $this->arg('tracking_number'); + $status = 'dispatch-booked'; + } else if ($this->arg('status') === 'PENDING') { + $tracking = ''; + $status = 'dispatch-requested'; + } else { + $this->_error('Invalid status'); + } + $data = $this->args; if (!array_key_exists('prop', $data)) $data['prop'] = $dew['PROPOSAL']; @@ -1434,54 +1462,89 @@ function _dispatch_dewar_confirmation() $data['LOCATION'] = $dew['STORAGELOCATION']; if (!array_key_exists('EMAILADDRESS', $data)) $data['EMAILADDRESS'] = $dew['EMAILADDRESS'];; - if (!array_key_exists('tracking_number', $data)) - $data['tracking_number'] = $dew['TRACKINGNUMBERFROMSYNCHROTRON']; - $subject_line = '*** Dispatch requested for Dewar ' . $dew['BARCODE'] . ' from ' . $data['LOCATION'] . ' ***'; - $email_template = 'dewar-dispatch-lite'; - $email = new Email($email_template, $subject_line); - $email->data = $data; - // If a local contact is given, try to find their email address - // First try LDAP, if unsuccessful look at the ISPyB person record for a matching staff user - $local_contact = $this->has_arg('LOCALCONTACT') ? $this->args['LOCALCONTACT'] : ''; - if ($local_contact) { - $this->args['LCEMAIL'] = $this->_get_email_fn($local_contact); - if (!$this->args['LCEMAIL']) { - $this->args['LCEMAIL'] = $this->_get_ispyb_email_fn($local_contact); + if ($this->arg('status') === 'BOOKED') { + # Prepare e-mail to stores + $subject_line = '*** Dispatch requested for Dewar ' . $dew['BARCODE'] . ' from ' . $data['LOCATION'] . ' ***'; + $email_template = 'dewar-dispatch-lite'; + $email = new Email($email_template, $subject_line); + $email->data = $data; + + // If a local contact is given, try to find their email address + // First try LDAP, if unsuccessful look at the ISPyB person record for a matching staff user + $local_contact = $this->has_arg('LOCALCONTACT') ? $this->args['LOCALCONTACT'] : ''; + if ($local_contact) { + $this->args['LCEMAIL'] = $this->_get_email_fn($local_contact); + if (!$this->args['LCEMAIL']) { + $this->args['LCEMAIL'] = $this->_get_ispyb_email_fn($local_contact); + } } - } - $recpts = $dispatch_email; - if ($data['EMAILADDRESS']) $recpts .= ', ' . $data['EMAILADDRESS']; - $local_contact_email = $this->has_arg('LCEMAIL') ? $this->args['LCEMAIL'] : ''; - if ($local_contact_email) $recpts .= ', ' . $local_contact_email; + $recpts = $dispatch_email; + if ($data['EMAILADDRESS']) $recpts .= ', ' . $data['EMAILADDRESS']; + $local_contact_email = $this->has_arg('LCEMAIL') ? $this->args['LCEMAIL'] : ''; + if ($local_contact_email) $recpts .= ', ' . $local_contact_email; - if (!is_null($dispatch_email_regex)) { - foreach ($dispatch_email_regex as $address => $pattern) { - if (preg_match($pattern, $data['BARCODE'])) { - $recpts .= ', ' . $address; + if (!is_null($dispatch_email_regex)) { + foreach ($dispatch_email_regex as $address => $pattern) { + if (preg_match($pattern, $data['BARCODE'])) { + $recpts .= ', ' . $address; + } } } - } - $email->send($recpts); + $email->send($recpts); + } // Also update the dewar status and storage location to keep it in sync with history... $this->db->pq( "UPDATE dewar - set dewarstatus='dispatch-requested', storagelocation=lower(:2), trackingnumberfromsynchrotron=:3 - WHERE dewarid=:1", - array($dew['DEWARID'], $data['LOCATION'], $data['tracking_number']) + set dewarstatus=:1, storagelocation=lower(:2), trackingnumberfromsynchrotron=:3 + WHERE dewarid=:4", + array($status, $data['LOCATION'], $tracking, $dew['DEWARID']) ); - $this->db->pq("UPDATE shipping set shippingstatus='dispatch-requested' WHERE shippingid=:1", array($dew['SHIPPINGID'])); + $this->db->pq("UPDATE shipping set shippingstatus=:1 WHERE shippingid=:2", array($status, $dew['SHIPPINGID'])); // Update dewar transport history with provided location. $this->db->pq( "INSERT INTO dewartransporthistory (dewartransporthistoryid,dewarid,dewarstatus,storagelocation,arrivaldate) - VALUES (s_dewartransporthistory.nextval,:1,'dispatch-requested',:2,CURRENT_TIMESTAMP) + VALUES (s_dewartransporthistory.nextval,:1,:2,:3,CURRENT_TIMESTAMP) + RETURNING dewartransporthistoryid INTO :id", + array($dew['DEWARID'], $status, $data['LOCATION']) + ); + + $this->_output(1); + } + + function _cancel_dispatch_dewar_confirmation() + { + // Check token against dewar + $dew = $this->db->pq( + "SELECT d.dewarid, d.shippingid, json_unquote(json_extract(d.extra, '$.token')) as token, storagelocation + FROM dewar d + WHERE d.dewarid=:1", + array($this->arg('did')) + ); + + $dew = $dew[0]; + + if (!$this->has_arg('TOKEN') || $this->arg('TOKEN') !== $dew['TOKEN']) { + $this->_error('Incorrect token'); + } + + $this->db->pq("UPDATE shipping set shippingstatus='dispatch request cancelled' WHERE shippingid=:1", array($dew['SHIPPINGID'])); + + // Update the dewar status + $this->db->pq("UPDATE dewar set dewarstatus='dispatch request cancelled' WHERE dewarid=:1", array($dew['DEWARID'])); + + // Update dewar transport history + $loc = Utils::getValueOrDefault($dew['STORAGELOCATION'], ''); + $this->db->pq( + "INSERT INTO dewartransporthistory (dewartransporthistoryid,dewarid,dewarstatus,storagelocation,arrivaldate) + VALUES (s_dewartransporthistory.nextval,:1,'dispatch request cancelled',:2,CURRENT_TIMESTAMP) RETURNING dewartransporthistoryid INTO :id", - array($dew['DEWARID'], $data['LOCATION']) + array($dew['DEWARID'], $loc) ); $this->_output(1); diff --git a/client/src/js/modules/shipment/views/dewars.js b/client/src/js/modules/shipment/views/dewars.js index 70a7726eb..6d41d1a58 100644 --- a/client/src/js/modules/shipment/views/dewars.js +++ b/client/src/js/modules/shipment/views/dewars.js @@ -147,6 +147,8 @@ define(['marionette', 'backbone', if (app.options.get('shipping_service_app_url') && this.model.get('EXTERNALSHIPPINGIDFROMSYNCHROTRON')) { let link = app.options.get('shipping_service_app_url')+'/shipment-requests/'+this.model.get('EXTERNALSHIPPINGIDFROMSYNCHROTRON')+'/outgoing' this.ui.ssd.attr('href', link) + this.ui.dispatch.hide() + this.ui.transfer.hide() } else { this.ui.ssd.hide() } diff --git a/client/src/js/modules/shipment/views/dispatch.js b/client/src/js/modules/shipment/views/dispatch.js index 3304e00b6..2918c5462 100644 --- a/client/src/js/modules/shipment/views/dispatch.js +++ b/client/src/js/modules/shipment/views/dispatch.js @@ -119,11 +119,7 @@ define(['marionette', 'views/form', }, success: function() { - if ( - app.options.get("shipping_service_app_url") - && (Number(this.terms.get('ACCEPTED')) === 1) // terms.ACCEPTED could be undefined, 1, or "1" - && app.options.get("facility_courier_countries").includes(this.dispatchCountry) - ) { + if (this.usingShippingService()) { this.getOption('dewar').fetch().done((dewar) => { const external_id = dewar.EXTERNALSHIPPINGIDFROMSYNCHROTRON; if (external_id === null){ @@ -289,21 +285,29 @@ define(['marionette', 'views/form', this.ui.facc.show() } - if ( - this.terms.get("ACCEPTED") - && app.options.get("shipping_service_app_url") - && app.options.get("facility_courier_countries").includes(this.dispatchCountry) - ){ - this.disableValidation() - this.ui.dispatchDetails.hide() - this.ui.submit.text("Proceed") - this.ui.shippingadvice.html("On clicking 'Proceed' you will be redirected to the new Diamond shipping service to book the shipment. Please ensure all stages of the form are completed.

") + if (this.usingShippingService()) { + this.useShortFormForShippingService() } else { this.ui.submit.text("Request Dewar Dispatch") this.ui.shippingadvice.html("") } }, + usingShippingService: function() { + const domestic = app.options.get("facility_courier_countries").includes(this.ui.country.val()) + const nde = app.options.get("use_shipping_service_nde") && app.options.get("facility_courier_countries_nde").includes(this.ui.country.val()) + return this.terms.get("ACCEPTED") + && app.options.get("shipping_service_app_url") + && (domestic || nde) + }, + + useShortFormForShippingService: function() { + this.disableValidation() + this.ui.dispatchDetails.hide() + this.ui.submit.text("Proceed") + this.ui.shippingadvice.html("On clicking 'Proceed' you will be redirected to the new Diamond shipping service to book the shipment. Please ensure all stages of the form are completed.

") + }, + enableValidation: function() { this.model.visitRequired = true this.model.dispatchDetailsRequired = true @@ -358,14 +362,8 @@ define(['marionette', 'views/form', this.ui.courierDetails.hide() this.ui.facilityCourier.show() this.model.courierDetailsRequired = false - if ( - app.options.get("shipping_service_app_url") - && app.options.get("facility_courier_countries").includes(this.ui.country.val()) - ){ - this.disableValidation() - this.ui.dispatchDetails.hide() - this.ui.submit.text("Proceed") - this.ui.shippingadvice.html("On clicking 'Proceed' you will be redirected to the new Diamond shipping service to book the shipment. Please ensure all stages of the form are completed.

") + if (this.usingShippingService()) { + this.useShortFormForShippingService() } }, diff --git a/client/src/js/utils/validation.js b/client/src/js/utils/validation.js index fcaf23509..d5a09c1c1 100644 --- a/client/src/js/utils/validation.js +++ b/client/src/js/utils/validation.js @@ -15,7 +15,7 @@ define(['backbone', 'backbone-validation'], function(Backbone) { sequence: /^[>;\s\w+\n\(\)\.\|]+$/, address: /^(\w|\s|\-|\n|,)+$/, array: /^[\d+(.\d+)?),]+$/, - country: /^(\w|\s|\-|,|\(|\)|')+$/, + country: /^(\w|\s|\-|,|\(|\)|'|\.)+$/, visit: /^\w+\d+-\d+$/, visitornull: /^\w+\d+-\d+$|^(?![\s\S])/, twopath: /^(\w|-)+\/?(\w|-)+$/, @@ -38,7 +38,7 @@ define(['backbone', 'backbone-validation'], function(Backbone) { sequence: 'This field may only contain word characters and line returns', address: 'This field may only contain word character, spaces, and line returns', array: 'This field may only contain numbers and commas', - country: 'This field must contain only letters, numbers, spaces, underscores, dashes, and commas', + country: 'This field must contain only letters, numbers, spaces, underscores, dashes, dots and commas', visit: 'This field must be of the format xxx123-123', visitornull: 'This field must be of the format xxx123-123 or a null value', twopath: 'This field can hold a path with two folders',