mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-14 18:56:49 +00:00
Compare commits
1 Commits
v5.0.10
...
RachL-patc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5020cc740 |
@@ -34,6 +34,12 @@ Lint/EmptyClass:
|
||||
Exclude:
|
||||
- 'spec/lib/reports/report_loader_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowComments.
|
||||
Lint/EmptyFile:
|
||||
Exclude:
|
||||
- 'spec/lib/open_food_network/enterprise_injection_data_spec.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Lint/FloatComparison:
|
||||
Exclude:
|
||||
@@ -86,6 +92,7 @@ Metrics/AbcSize:
|
||||
- 'app/controllers/admin/enterprises_controller.rb'
|
||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||
- 'app/controllers/spree/admin/payments_controller.rb'
|
||||
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||
- 'app/controllers/spree/admin/variants_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||
@@ -120,7 +127,7 @@ Metrics/BlockNesting:
|
||||
Exclude:
|
||||
- 'app/models/spree/payment/processing.rb'
|
||||
|
||||
# Offense count: 47
|
||||
# Offense count: 46
|
||||
# Configuration parameters: CountComments, Max, CountAsOne.
|
||||
Metrics/ClassLength:
|
||||
Exclude:
|
||||
@@ -130,7 +137,6 @@ Metrics/ClassLength:
|
||||
- 'app/controllers/admin/resource_controller.rb'
|
||||
- 'app/controllers/admin/subscriptions_controller.rb'
|
||||
- 'app/controllers/application_controller.rb'
|
||||
- 'app/controllers/checkout_controller.rb'
|
||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||
- 'app/controllers/spree/admin/orders_controller.rb'
|
||||
- 'app/controllers/spree/admin/payment_methods_controller.rb'
|
||||
@@ -177,7 +183,7 @@ Metrics/ClassLength:
|
||||
Metrics/CyclomaticComplexity:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/enterprises_controller.rb'
|
||||
- 'app/controllers/spree/admin/payments_controller.rb'
|
||||
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/helpers/checkout_helper.rb'
|
||||
- 'app/helpers/order_cycles_helper.rb'
|
||||
@@ -202,12 +208,13 @@ Metrics/CyclomaticComplexity:
|
||||
- 'lib/spree/localized_number.rb'
|
||||
- 'spec/models/product_importer_spec.rb'
|
||||
|
||||
# Offense count: 23
|
||||
# Offense count: 24
|
||||
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/enterprises_controller.rb'
|
||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||
- 'app/models/spree/ability.rb'
|
||||
@@ -286,17 +293,19 @@ Metrics/ParameterLists:
|
||||
- 'spec/support/controller_requests_helper.rb'
|
||||
- 'spec/system/admin/reports_spec.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Offense count: 4
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
||||
Metrics/PerceivedComplexity:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||
- 'app/models/enterprise_relationship.rb'
|
||||
- 'app/models/spree/ability.rb'
|
||||
- 'app/models/spree/order/checkout.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# Offense count: 8
|
||||
Naming/AccessorMethodName:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/admin/taxonomies_controller.rb'
|
||||
- 'app/mailers/producer_mailer.rb'
|
||||
- 'app/models/spree/order.rb'
|
||||
- 'app/services/checkout/post_checkout_actions.rb'
|
||||
@@ -344,7 +353,7 @@ Naming/VariableNumber:
|
||||
- 'spec/models/spree/tax_rate_spec.rb'
|
||||
- 'spec/requests/api/orders_spec.rb'
|
||||
|
||||
# Offense count: 143
|
||||
# Offense count: 142
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: ResponseMethods.
|
||||
# ResponseMethods: response, last_response
|
||||
@@ -548,7 +557,7 @@ RSpecRails/InferredSpecType:
|
||||
- 'spec/requests/voucher_adjustments_spec.rb'
|
||||
- 'spec/routing/stripe_spec.rb'
|
||||
|
||||
# Offense count: 21
|
||||
# Offense count: 22
|
||||
# Configuration parameters: IgnoreScopes, Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/InverseOf:
|
||||
@@ -563,6 +572,7 @@ Rails/InverseOf:
|
||||
- 'app/models/spree/price.rb'
|
||||
- 'app/models/spree/product.rb'
|
||||
- 'app/models/spree/stock_item.rb'
|
||||
- 'app/models/spree/taxonomy.rb'
|
||||
- 'app/models/spree/variant.rb'
|
||||
- 'app/models/subscription_line_item.rb'
|
||||
|
||||
@@ -710,7 +720,7 @@ Style/GlobalStdStream:
|
||||
- 'lib/tasks/subscriptions/debug.rake'
|
||||
- 'lib/tasks/subscriptions/test.rake'
|
||||
|
||||
# Offense count: 10
|
||||
# Offense count: 12
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowSplatArgument.
|
||||
Style/HashConversion:
|
||||
@@ -718,7 +728,9 @@ Style/HashConversion:
|
||||
- 'app/controllers/admin/column_preferences_controller.rb'
|
||||
- 'app/controllers/admin/variant_overrides_controller.rb'
|
||||
- 'app/controllers/spree/admin/products_controller.rb'
|
||||
- 'app/models/order_cycle.rb'
|
||||
- 'app/models/product_import/product_importer.rb'
|
||||
- 'app/models/spree/shipping_method.rb'
|
||||
- 'app/serializers/api/admin/exchange_serializer.rb'
|
||||
- 'app/services/variants_stock_levels.rb'
|
||||
- 'spec/controllers/admin/inventory_items_controller_spec.rb'
|
||||
|
||||
@@ -89,4 +89,4 @@ RUN ./script/install-bundler
|
||||
RUN yarn install
|
||||
|
||||
# Run bundler install in parallel with the amount of available CPUs
|
||||
RUN bundle install --jobs="$(nproc)"
|
||||
RUN bundle install --jobs="$(nproc)"
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -86,7 +86,7 @@ gem "active_model_serializers", "0.8.4"
|
||||
gem 'activerecord-session_store'
|
||||
gem 'acts-as-taggable-on'
|
||||
gem 'angularjs-file-upload-rails', '~> 2.4.1'
|
||||
gem 'bigdecimal'
|
||||
gem 'bigdecimal', '3.0.2'
|
||||
gem 'bootsnap', require: false
|
||||
gem 'geocoder'
|
||||
gem 'gmaps4rails'
|
||||
|
||||
18
Gemfile.lock
18
Gemfile.lock
@@ -180,7 +180,7 @@ GEM
|
||||
base64 (0.2.0)
|
||||
bcp47_spec (0.2.1)
|
||||
bcrypt (3.1.20)
|
||||
bigdecimal (3.1.8)
|
||||
bigdecimal (3.0.2)
|
||||
bindata (2.5.0)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.3)
|
||||
@@ -191,7 +191,7 @@ GEM
|
||||
bullet (7.1.6)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
cable_ready (5.0.6)
|
||||
cable_ready (5.0.5)
|
||||
actionpack (>= 5.2)
|
||||
actionview (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
@@ -755,16 +755,16 @@ GEM
|
||||
state_machines-activerecord (0.9.0)
|
||||
activerecord (>= 6.0)
|
||||
state_machines-activemodel (>= 0.9.0)
|
||||
stimulus_reflex (3.5.3)
|
||||
actioncable (>= 5.2)
|
||||
actionpack (>= 5.2)
|
||||
actionview (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
stimulus_reflex (3.5.1)
|
||||
actioncable (>= 5.2, < 8)
|
||||
actionpack (>= 5.2, < 8)
|
||||
actionview (>= 5.2, < 8)
|
||||
activesupport (>= 5.2, < 8)
|
||||
cable_ready (~> 5.0)
|
||||
nokogiri (~> 1.0)
|
||||
nokogiri-html5-inference (~> 0.3)
|
||||
rack (>= 2, < 4)
|
||||
railties (>= 5.2)
|
||||
railties (>= 5.2, < 8)
|
||||
redis (>= 4.0, < 6.0)
|
||||
stringex (2.8.6)
|
||||
stringio (3.1.0)
|
||||
@@ -865,7 +865,7 @@ DEPENDENCIES
|
||||
angularjs-rails (= 1.8.0)
|
||||
arel-helpers (~> 2.12)
|
||||
aws-sdk-s3
|
||||
bigdecimal
|
||||
bigdecimal (= 3.0.2)
|
||||
bootsnap
|
||||
bugsnag
|
||||
bullet
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
FROM ruby:3.1.4-alpine3.19 AS base
|
||||
ENV LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8 \
|
||||
TZ=Europe/London \
|
||||
RAILS_ROOT=/usr/src/app
|
||||
RUN apk --no-cache upgrade && \
|
||||
apk add --no-cache tzdata postgresql-client imagemagick imagemagick-jpeg && \
|
||||
apk add --no-cache --virtual wkhtmltopdf
|
||||
|
||||
WORKDIR $RAILS_ROOT
|
||||
|
||||
# Development dependencies
|
||||
FROM base AS development-base
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
build-base postgresql-dev git nodejs yarn && \
|
||||
apk add --no-cache --virtual .dev-utils \
|
||||
bash curl less vim chromium-chromedriver zlib-dev openssl-dev \
|
||||
readline-dev yaml-dev sqlite-dev libxml2-dev libxslt-dev libffi-dev vips-dev && \
|
||||
curl -o /usr/local/bin/wait-for-it https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh && \
|
||||
chmod +x /usr/local/bin/wait-for-it
|
||||
|
||||
# Install yarn dependencies separately for caching
|
||||
FROM development-base AS yarn-dependencies
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
# Install Ruby gems
|
||||
FROM development-base
|
||||
COPY . $RAILS_ROOT
|
||||
COPY Gemfile Gemfile.lock ./
|
||||
RUN bundle install --jobs "$(nproc)"
|
||||
COPY --from=yarn-dependencies $RAILS_ROOT/node_modules ./node_modules
|
||||
@@ -67,5 +67,8 @@
|
||||
// foundation
|
||||
//= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js
|
||||
|
||||
// LocalStorage
|
||||
//= require ../shared/angular-local-storage.js
|
||||
|
||||
// requires the rest of the JS code in this folder
|
||||
//= require_tree .
|
||||
|
||||
@@ -307,6 +307,9 @@ filterSubmitProducts = (productsToFilter) ->
|
||||
variantHasUpdatableProperty = result.hasUpdatableProperty
|
||||
filteredVariants.push filteredVariant if variantHasUpdatableProperty
|
||||
|
||||
if product.hasOwnProperty("sku")
|
||||
filteredProduct.sku = product.sku
|
||||
hasUpdatableProperty = true
|
||||
if product.hasOwnProperty("name")
|
||||
filteredProduct.name = product.name
|
||||
hasUpdatableProperty = true
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
angular.module("admin.indexUtils").factory 'KeyValueMapStore', (localStorageService)->
|
||||
new class KeyValueMapStore
|
||||
localStorageKey: ''
|
||||
storableKeys: []
|
||||
|
||||
constructor: ->
|
||||
localStorageService.setStorageType("sessionStorage")
|
||||
|
||||
getStoredKeyValueMap: ->
|
||||
localStorageService.get(@localStorageKey) || {}
|
||||
|
||||
setStoredValues: (source) ->
|
||||
keyValueMap = {}
|
||||
for key in @storableKeys
|
||||
keyValueMap[key] = source[key]
|
||||
localStorageService.set(@localStorageKey, keyValueMap)
|
||||
|
||||
restoreValues: (target) ->
|
||||
storedKeyValueMap = @getStoredKeyValueMap()
|
||||
|
||||
return false if _.isEmpty(storedKeyValueMap)
|
||||
|
||||
for k,v of storedKeyValueMap
|
||||
target[k] = v
|
||||
|
||||
return true
|
||||
|
||||
clearKeyValueMap: () ->
|
||||
localStorageService.remove(@localStorageKey)
|
||||
@@ -17,6 +17,7 @@
|
||||
#= require angular-google-maps.min.js
|
||||
#= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js
|
||||
#= require ../shared/ng-infinite-scroll.min.js
|
||||
#= require ../shared/angular-local-storage.js
|
||||
#= require ../shared/angular-slideables.js
|
||||
#= require ../shared/shared
|
||||
#= require_tree ../shared/directives
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
angular.module("Darkswarm", [
|
||||
'ngResource',
|
||||
'mm.foundation',
|
||||
'LocalStorageModule',
|
||||
'infinite-scroll',
|
||||
'angular-flash.service',
|
||||
'templates',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
angular.module('Darkswarm').directive "ofnDisableScroll", ()->
|
||||
# Stops scrolling from incrementing or decrementing input value
|
||||
# Useful for number inputs
|
||||
restrict: 'A'
|
||||
link: (scope, element, attrs)->
|
||||
element.bind 'focus', ->
|
||||
element.bind 'mousewheel', (e)->
|
||||
e.preventDefault()
|
||||
element.bind 'blur', ->
|
||||
element.unbind 'mousewheel'
|
||||
@@ -0,0 +1,5 @@
|
||||
angular.module('Darkswarm').directive "integer", ->
|
||||
restrict: 'A'
|
||||
link: (scope, elem, attr) ->
|
||||
elem.bind 'input', ->
|
||||
elem.val Math.round(elem.val())
|
||||
@@ -20,13 +20,10 @@ angular.module('Darkswarm').directive 'mapSearch', ($timeout, Search) ->
|
||||
$timeout =>
|
||||
map = ctrl.getMap()
|
||||
|
||||
if !map
|
||||
alert(t('gmap_load_failure'))
|
||||
else
|
||||
searchBox = scope.createSearchBox map
|
||||
scope.bindSearchResponse map, searchBox
|
||||
scope.biasResults map, searchBox
|
||||
scope.performUrlSearch map
|
||||
searchBox = scope.createSearchBox map
|
||||
scope.bindSearchResponse map, searchBox
|
||||
scope.biasResults map, searchBox
|
||||
scope.performUrlSearch map
|
||||
|
||||
scope.createSearchBox = (map) ->
|
||||
map.controls[google.maps.ControlPosition.TOP_LEFT].push scope.input
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
angular.module('Darkswarm').directive "renderSvg", ()->
|
||||
# Magical directive that'll render SVGs from URLs
|
||||
# If only there were a neater way of doing this
|
||||
restrict: 'E'
|
||||
priority: 99
|
||||
template: "<svg-wrapper></svg-wrapper>"
|
||||
|
||||
# Fetch SVG via ajax, inject into page using DOM
|
||||
link: (scope, elem, attr)->
|
||||
if /.svg/.test attr.path # Only do this if we've got an svg
|
||||
$.ajax
|
||||
url: attr.path
|
||||
success: (html)->
|
||||
elem.html($(html).find("svg"))
|
||||
@@ -0,0 +1,9 @@
|
||||
angular.module('Darkswarm').directive "ofnScrollTo", ($location, $anchorScroll)->
|
||||
# Onclick sets $location.hash to attrs.ofnScrollTo
|
||||
# Then triggers anchorScroll
|
||||
restrict: 'A'
|
||||
link: (scope, element, attrs)->
|
||||
element.bind 'click', (ev)->
|
||||
ev.stopPropagation()
|
||||
$location.hash attrs.ofnScrollTo
|
||||
$anchorScroll()
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('Darkswarm').factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, $resource, Messages) ->
|
||||
angular.module('Darkswarm').factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, $resource, localStorageService, Messages) ->
|
||||
# Handles syncing of current cart/order state to server
|
||||
new class Cart
|
||||
dirty: false
|
||||
@@ -113,6 +113,7 @@ angular.module('Darkswarm').factory 'Cart', (CurrentOrder, Variants, $timeout, $
|
||||
|
||||
clear: ->
|
||||
@line_items = []
|
||||
localStorageService.clearAll() # One day this will have to be moar GRANULAR
|
||||
|
||||
isOnlyItemInOrder: (id) =>
|
||||
deletedItem = @line_items_finalised.find((item) -> item.id == id)
|
||||
|
||||
546
app/assets/javascripts/shared/angular-local-storage.js
vendored
Normal file
546
app/assets/javascripts/shared/angular-local-storage.js
vendored
Normal file
@@ -0,0 +1,546 @@
|
||||
/**
|
||||
* An Angular module that gives you access to the browsers local storage
|
||||
* @version v0.5.0 - 2016-08-29
|
||||
* @link https://github.com/grevory/angular-local-storage
|
||||
* @author grevory <greg@gregpike.ca>
|
||||
* @license MIT License, http://www.opensource.org/licenses/MIT
|
||||
*/
|
||||
(function (window, angular) {
|
||||
var isDefined = angular.isDefined,
|
||||
isUndefined = angular.isUndefined,
|
||||
isNumber = angular.isNumber,
|
||||
isObject = angular.isObject,
|
||||
isArray = angular.isArray,
|
||||
extend = angular.extend,
|
||||
toJson = angular.toJson;
|
||||
|
||||
angular
|
||||
.module('LocalStorageModule', [])
|
||||
.provider('localStorageService', function() {
|
||||
// You should set a prefix to avoid overwriting any local storage variables from the rest of your app
|
||||
// e.g. localStorageServiceProvider.setPrefix('yourAppName');
|
||||
// With provider you can use config as this:
|
||||
// myApp.config(function (localStorageServiceProvider) {
|
||||
// localStorageServiceProvider.prefix = 'yourAppName';
|
||||
// });
|
||||
this.prefix = 'ls';
|
||||
|
||||
// You could change web storage type localstorage or sessionStorage
|
||||
this.storageType = 'localStorage';
|
||||
|
||||
// Cookie options (usually in case of fallback)
|
||||
// expiry = Number of days before cookies expire // 0 = Does not expire
|
||||
// path = The web path the cookie represents
|
||||
// secure = Wether the cookies should be secure (i.e only sent on HTTPS requests)
|
||||
this.cookie = {
|
||||
expiry: 30,
|
||||
path: '/',
|
||||
secure: false
|
||||
};
|
||||
|
||||
// Decides wether we should default to cookies if localstorage is not supported.
|
||||
this.defaultToCookie = true;
|
||||
|
||||
// Send signals for each of the following actions?
|
||||
this.notify = {
|
||||
setItem: true,
|
||||
removeItem: false
|
||||
};
|
||||
|
||||
// Setter for the prefix
|
||||
this.setPrefix = function(prefix) {
|
||||
this.prefix = prefix;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Setter for the storageType
|
||||
this.setStorageType = function(storageType) {
|
||||
this.storageType = storageType;
|
||||
return this;
|
||||
};
|
||||
// Setter for defaultToCookie value, default is true.
|
||||
this.setDefaultToCookie = function (shouldDefault) {
|
||||
this.defaultToCookie = !!shouldDefault; // Double-not to make sure it's a bool value.
|
||||
return this;
|
||||
};
|
||||
// Setter for cookie config
|
||||
this.setStorageCookie = function(exp, path, secure) {
|
||||
this.cookie.expiry = exp;
|
||||
this.cookie.path = path;
|
||||
this.cookie.secure = secure;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Setter for cookie domain
|
||||
this.setStorageCookieDomain = function(domain) {
|
||||
this.cookie.domain = domain;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Setter for notification config
|
||||
// itemSet & itemRemove should be booleans
|
||||
this.setNotify = function(itemSet, itemRemove) {
|
||||
this.notify = {
|
||||
setItem: itemSet,
|
||||
removeItem: itemRemove
|
||||
};
|
||||
return this;
|
||||
};
|
||||
|
||||
this.$get = ['$rootScope', '$window', '$document', '$parse','$timeout', function($rootScope, $window, $document, $parse, $timeout) {
|
||||
var self = this;
|
||||
var prefix = self.prefix;
|
||||
var cookie = self.cookie;
|
||||
var notify = self.notify;
|
||||
var storageType = self.storageType;
|
||||
var webStorage;
|
||||
|
||||
// When Angular's $document is not available
|
||||
if (!$document) {
|
||||
$document = document;
|
||||
} else if ($document[0]) {
|
||||
$document = $document[0];
|
||||
}
|
||||
|
||||
// If there is a prefix set in the config lets use that with an appended period for readability
|
||||
if (prefix.substr(-1) !== '.') {
|
||||
prefix = !!prefix ? prefix + '.' : '';
|
||||
}
|
||||
var deriveQualifiedKey = function(key) {
|
||||
return prefix + key;
|
||||
};
|
||||
|
||||
// Removes prefix from the key.
|
||||
var underiveQualifiedKey = function (key) {
|
||||
return key.replace(new RegExp('^' + prefix, 'g'), '');
|
||||
};
|
||||
|
||||
// Check if the key is within our prefix namespace.
|
||||
var isKeyPrefixOurs = function (key) {
|
||||
return key.indexOf(prefix) === 0;
|
||||
};
|
||||
|
||||
// Checks the browser to see if local storage is supported
|
||||
var checkSupport = function () {
|
||||
try {
|
||||
var supported = (storageType in $window && $window[storageType] !== null);
|
||||
|
||||
// When Safari (OS X or iOS) is in private browsing mode, it appears as though localStorage
|
||||
// is available, but trying to call .setItem throws an exception.
|
||||
//
|
||||
// "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage
|
||||
// that exceeded the quota."
|
||||
var key = deriveQualifiedKey('__' + Math.round(Math.random() * 1e7));
|
||||
if (supported) {
|
||||
webStorage = $window[storageType];
|
||||
webStorage.setItem(key, '');
|
||||
webStorage.removeItem(key);
|
||||
}
|
||||
|
||||
return supported;
|
||||
} catch (e) {
|
||||
// Only change storageType to cookies if defaulting is enabled.
|
||||
if (self.defaultToCookie)
|
||||
storageType = 'cookie';
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var browserSupportsLocalStorage = checkSupport();
|
||||
|
||||
// Directly adds a value to local storage
|
||||
// If local storage is not available in the browser use cookies
|
||||
// Example use: localStorageService.add('library','angular');
|
||||
var addToLocalStorage = function (key, value, type) {
|
||||
setStorageType(type);
|
||||
|
||||
// Let's convert undefined values to null to get the value consistent
|
||||
if (isUndefined(value)) {
|
||||
value = null;
|
||||
} else {
|
||||
value = toJson(value);
|
||||
}
|
||||
|
||||
// If this browser does not support local storage use cookies
|
||||
if (!browserSupportsLocalStorage && self.defaultToCookie || self.storageType === 'cookie') {
|
||||
if (!browserSupportsLocalStorage) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||
}
|
||||
|
||||
if (notify.setItem) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.setitem', {key: key, newvalue: value, storageType: 'cookie'});
|
||||
}
|
||||
return addToCookies(key, value);
|
||||
}
|
||||
|
||||
try {
|
||||
if (webStorage) {
|
||||
webStorage.setItem(deriveQualifiedKey(key), value);
|
||||
}
|
||||
if (notify.setItem) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.setitem', {key: key, newvalue: value, storageType: self.storageType});
|
||||
}
|
||||
} catch (e) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||
return addToCookies(key, value);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Directly get a value from local storage
|
||||
// Example use: localStorageService.get('library'); // returns 'angular'
|
||||
var getFromLocalStorage = function (key, type) {
|
||||
setStorageType(type);
|
||||
|
||||
if (!browserSupportsLocalStorage && self.defaultToCookie || self.storageType === 'cookie') {
|
||||
if (!browserSupportsLocalStorage) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||
}
|
||||
|
||||
return getFromCookies(key);
|
||||
}
|
||||
|
||||
var item = webStorage ? webStorage.getItem(deriveQualifiedKey(key)) : null;
|
||||
// angular.toJson will convert null to 'null', so a proper conversion is needed
|
||||
// FIXME not a perfect solution, since a valid 'null' string can't be stored
|
||||
if (!item || item === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(item);
|
||||
} catch (e) {
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
// Remove an item from local storage
|
||||
// Example use: localStorageService.remove('library'); // removes the key/value pair of library='angular'
|
||||
//
|
||||
// This is var-arg removal, check the last argument to see if it is a storageType
|
||||
// and set type accordingly before removing.
|
||||
//
|
||||
var removeFromLocalStorage = function () {
|
||||
// can't pop on arguments, so we do this
|
||||
var consumed = 0;
|
||||
if (arguments.length >= 1 &&
|
||||
(arguments[arguments.length - 1] === 'localStorage' ||
|
||||
arguments[arguments.length - 1] === 'sessionStorage')) {
|
||||
consumed = 1;
|
||||
setStorageType(arguments[arguments.length - 1]);
|
||||
}
|
||||
|
||||
var i, key;
|
||||
for (i = 0; i < arguments.length - consumed; i++) {
|
||||
key = arguments[i];
|
||||
if (!browserSupportsLocalStorage && self.defaultToCookie || self.storageType === 'cookie') {
|
||||
if (!browserSupportsLocalStorage) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||
}
|
||||
|
||||
if (notify.removeItem) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.removeitem', {key: key, storageType: 'cookie'});
|
||||
}
|
||||
removeFromCookies(key);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
webStorage.removeItem(deriveQualifiedKey(key));
|
||||
if (notify.removeItem) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.removeitem', {
|
||||
key: key,
|
||||
storageType: self.storageType
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||
removeFromCookies(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Return array of keys for local storage
|
||||
// Example use: var keys = localStorageService.keys()
|
||||
var getKeysForLocalStorage = function (type) {
|
||||
setStorageType(type);
|
||||
|
||||
if (!browserSupportsLocalStorage) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||
return [];
|
||||
}
|
||||
|
||||
var prefixLength = prefix.length;
|
||||
var keys = [];
|
||||
for (var key in webStorage) {
|
||||
// Only return keys that are for this app
|
||||
if (key.substr(0, prefixLength) === prefix) {
|
||||
try {
|
||||
keys.push(key.substr(prefixLength));
|
||||
} catch (e) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', e.Description);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
// Remove all data for this app from local storage
|
||||
// Also optionally takes a regular expression string and removes the matching key-value pairs
|
||||
// Example use: localStorageService.clearAll();
|
||||
// Should be used mostly for development purposes
|
||||
var clearAllFromLocalStorage = function (regularExpression, type) {
|
||||
setStorageType(type);
|
||||
|
||||
// Setting both regular expressions independently
|
||||
// Empty strings result in catchall RegExp
|
||||
var prefixRegex = !!prefix ? new RegExp('^' + prefix) : new RegExp();
|
||||
var testRegex = !!regularExpression ? new RegExp(regularExpression) : new RegExp();
|
||||
|
||||
if (!browserSupportsLocalStorage && self.defaultToCookie || self.storageType === 'cookie') {
|
||||
if (!browserSupportsLocalStorage) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||
}
|
||||
return clearAllFromCookies();
|
||||
}
|
||||
if (!browserSupportsLocalStorage && !self.defaultToCookie)
|
||||
return false;
|
||||
var prefixLength = prefix.length;
|
||||
|
||||
for (var key in webStorage) {
|
||||
// Only remove items that are for this app and match the regular expression
|
||||
if (prefixRegex.test(key) && testRegex.test(key.substr(prefixLength))) {
|
||||
try {
|
||||
removeFromLocalStorage(key.substr(prefixLength));
|
||||
} catch (e) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||
return clearAllFromCookies();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Checks the browser to see if cookies are supported
|
||||
var browserSupportsCookies = (function() {
|
||||
try {
|
||||
return $window.navigator.cookieEnabled ||
|
||||
("cookie" in $document && ($document.cookie.length > 0 ||
|
||||
($document.cookie = "test").indexOf.call($document.cookie, "test") > -1));
|
||||
} catch (e) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||
return false;
|
||||
}
|
||||
}());
|
||||
|
||||
// Directly adds a value to cookies
|
||||
// Typically used as a fallback if local storage is not available in the browser
|
||||
// Example use: localStorageService.cookie.add('library','angular');
|
||||
var addToCookies = function (key, value, daysToExpiry, secure) {
|
||||
|
||||
if (isUndefined(value)) {
|
||||
return false;
|
||||
} else if(isArray(value) || isObject(value)) {
|
||||
value = toJson(value);
|
||||
}
|
||||
|
||||
if (!browserSupportsCookies) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', 'COOKIES_NOT_SUPPORTED');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
var expiry = '',
|
||||
expiryDate = new Date(),
|
||||
cookieDomain = '';
|
||||
|
||||
if (value === null) {
|
||||
// Mark that the cookie has expired one day ago
|
||||
expiryDate.setTime(expiryDate.getTime() + (-1 * 24 * 60 * 60 * 1000));
|
||||
expiry = "; expires=" + expiryDate.toGMTString();
|
||||
value = '';
|
||||
} else if (isNumber(daysToExpiry) && daysToExpiry !== 0) {
|
||||
expiryDate.setTime(expiryDate.getTime() + (daysToExpiry * 24 * 60 * 60 * 1000));
|
||||
expiry = "; expires=" + expiryDate.toGMTString();
|
||||
} else if (cookie.expiry !== 0) {
|
||||
expiryDate.setTime(expiryDate.getTime() + (cookie.expiry * 24 * 60 * 60 * 1000));
|
||||
expiry = "; expires=" + expiryDate.toGMTString();
|
||||
}
|
||||
if (!!key) {
|
||||
var cookiePath = "; path=" + cookie.path;
|
||||
if (cookie.domain) {
|
||||
cookieDomain = "; domain=" + cookie.domain;
|
||||
}
|
||||
/* Providing the secure parameter always takes precedence over config
|
||||
* (allows developer to mix and match secure + non-secure) */
|
||||
if (typeof secure === 'boolean') {
|
||||
if (secure === true) {
|
||||
/* We've explicitly specified secure,
|
||||
* add the secure attribute to the cookie (after domain) */
|
||||
cookieDomain += "; secure";
|
||||
}
|
||||
// else - secure has been supplied but isn't true - so don't set secure flag, regardless of what config says
|
||||
}
|
||||
else if (cookie.secure === true) {
|
||||
// secure parameter wasn't specified, get default from config
|
||||
cookieDomain += "; secure";
|
||||
}
|
||||
$document.cookie = deriveQualifiedKey(key) + "=" + encodeURIComponent(value) + expiry + cookiePath + cookieDomain;
|
||||
}
|
||||
} catch (e) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Directly get a value from a cookie
|
||||
// Example use: localStorageService.cookie.get('library'); // returns 'angular'
|
||||
var getFromCookies = function (key) {
|
||||
if (!browserSupportsCookies) {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.error', 'COOKIES_NOT_SUPPORTED');
|
||||
return false;
|
||||
}
|
||||
|
||||
var cookies = $document.cookie && $document.cookie.split(';') || [];
|
||||
for(var i=0; i < cookies.length; i++) {
|
||||
var thisCookie = cookies[i];
|
||||
while (thisCookie.charAt(0) === ' ') {
|
||||
thisCookie = thisCookie.substring(1,thisCookie.length);
|
||||
}
|
||||
if (thisCookie.indexOf(deriveQualifiedKey(key) + '=') === 0) {
|
||||
var storedValues = decodeURIComponent(thisCookie.substring(prefix.length + key.length + 1, thisCookie.length));
|
||||
try {
|
||||
return JSON.parse(storedValues);
|
||||
} catch(e) {
|
||||
return storedValues;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
var removeFromCookies = function (key) {
|
||||
addToCookies(key,null);
|
||||
};
|
||||
|
||||
var clearAllFromCookies = function () {
|
||||
var thisCookie = null;
|
||||
var prefixLength = prefix.length;
|
||||
var cookies = $document.cookie.split(';');
|
||||
for(var i = 0; i < cookies.length; i++) {
|
||||
thisCookie = cookies[i];
|
||||
|
||||
while (thisCookie.charAt(0) === ' ') {
|
||||
thisCookie = thisCookie.substring(1, thisCookie.length);
|
||||
}
|
||||
|
||||
var key = thisCookie.substring(prefixLength, thisCookie.indexOf('='));
|
||||
removeFromCookies(key);
|
||||
}
|
||||
};
|
||||
|
||||
var getStorageType = function() {
|
||||
return storageType;
|
||||
};
|
||||
|
||||
var setStorageType = function(type) {
|
||||
if (type && storageType !== type) {
|
||||
storageType = type;
|
||||
browserSupportsLocalStorage = checkSupport();
|
||||
}
|
||||
return browserSupportsLocalStorage;
|
||||
};
|
||||
|
||||
// Add a listener on scope variable to save its changes to local storage
|
||||
// Return a function which when called cancels binding
|
||||
var bindToScope = function(scope, key, def, lsKey, type) {
|
||||
lsKey = lsKey || key;
|
||||
var value = getFromLocalStorage(lsKey, type);
|
||||
|
||||
if (value === null && isDefined(def)) {
|
||||
value = def;
|
||||
} else if (isObject(value) && isObject(def)) {
|
||||
value = extend(value, def);
|
||||
}
|
||||
|
||||
$parse(key).assign(scope, value);
|
||||
|
||||
return scope.$watch(key, function(newVal) {
|
||||
addToLocalStorage(lsKey, newVal, type);
|
||||
}, isObject(scope[key]));
|
||||
};
|
||||
|
||||
// Add listener to local storage, for update callbacks.
|
||||
if (browserSupportsLocalStorage) {
|
||||
if ($window.addEventListener) {
|
||||
$window.addEventListener("storage", handleStorageChangeCallback, false);
|
||||
$rootScope.$on('$destroy', function() {
|
||||
$window.removeEventListener("storage", handleStorageChangeCallback);
|
||||
});
|
||||
} else if($window.attachEvent){
|
||||
// attachEvent and detachEvent are proprietary to IE v6-10
|
||||
$window.attachEvent("onstorage", handleStorageChangeCallback);
|
||||
$rootScope.$on('$destroy', function() {
|
||||
$window.detachEvent("onstorage", handleStorageChangeCallback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Callback handler for storage changed.
|
||||
function handleStorageChangeCallback(e) {
|
||||
if (!e) { e = $window.event; }
|
||||
if (notify.setItem) {
|
||||
if (isKeyPrefixOurs(e.key)) {
|
||||
var key = underiveQualifiedKey(e.key);
|
||||
// Use timeout, to avoid using $rootScope.$apply.
|
||||
$timeout(function () {
|
||||
$rootScope.$broadcast('LocalStorageModule.notification.changed', { key: key, newvalue: e.newValue, storageType: self.storageType });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return localStorageService.length
|
||||
// ignore keys that not owned
|
||||
var lengthOfLocalStorage = function(type) {
|
||||
setStorageType(type);
|
||||
|
||||
var count = 0;
|
||||
var storage = $window[storageType];
|
||||
for(var i = 0; i < storage.length; i++) {
|
||||
if(storage.key(i).indexOf(prefix) === 0 ) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
return {
|
||||
isSupported: browserSupportsLocalStorage,
|
||||
getStorageType: getStorageType,
|
||||
setStorageType: setStorageType,
|
||||
set: addToLocalStorage,
|
||||
add: addToLocalStorage, //DEPRECATED
|
||||
get: getFromLocalStorage,
|
||||
keys: getKeysForLocalStorage,
|
||||
remove: removeFromLocalStorage,
|
||||
clearAll: clearAllFromLocalStorage,
|
||||
bind: bindToScope,
|
||||
deriveKey: deriveQualifiedKey,
|
||||
underiveKey: underiveQualifiedKey,
|
||||
length: lengthOfLocalStorage,
|
||||
defaultToCookie: this.defaultToCookie,
|
||||
cookie: {
|
||||
isSupported: browserSupportsCookies,
|
||||
set: addToCookies,
|
||||
add: addToCookies, //DEPRECATED
|
||||
get: getFromCookies,
|
||||
remove: removeFromCookies,
|
||||
clearAll: clearAllFromCookies
|
||||
}
|
||||
};
|
||||
}];
|
||||
});
|
||||
})(window, window.angular);
|
||||
@@ -1,4 +1,5 @@
|
||||
window.OFNShared = angular.module("OFNShared", [
|
||||
"mm.foundation",
|
||||
"LocalStorageModule"
|
||||
]).config ($httpProvider) ->
|
||||
$httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"
|
||||
|
||||
@@ -44,9 +44,9 @@ module Admin
|
||||
|
||||
create_connected_app
|
||||
|
||||
jwt_service = Vine::JwtService.new(secret: connected_app_params[:vine_secret])
|
||||
vine_api = Vine::ApiService.new(api_key: connected_app_params[:vine_api_key],
|
||||
jwt_generator: jwt_service)
|
||||
jwt_service = VineJwtService.new(secret: connected_app_params[:vine_secret])
|
||||
vine_api = VineApiService.new(api_key: connected_app_params[:vine_api_key],
|
||||
jwt_generator: jwt_service)
|
||||
|
||||
if !@app.connect(api_key: connected_app_params[:vine_api_key],
|
||||
secret: connected_app_params[:vine_secret], vine_api:)
|
||||
@@ -77,7 +77,7 @@ module Admin
|
||||
|
||||
def log_and_notify_exception(exception)
|
||||
Rails.logger.error exception.inspect
|
||||
Alert.raise(exception)
|
||||
Bugsnag.notify(exception)
|
||||
end
|
||||
|
||||
def vine_params_empty?
|
||||
|
||||
@@ -19,12 +19,15 @@ module Admin
|
||||
.find(params.require(:enterprise_id))
|
||||
|
||||
catalog_url = params.require(:catalog_url)
|
||||
catalog = DfcCatalog.load(spree_current_user, catalog_url)
|
||||
catalog.apply_wholesale_values!
|
||||
|
||||
json_catalog = fetch_catalog(catalog_url)
|
||||
graph = DfcIo.import(json_catalog)
|
||||
|
||||
# * First step: import all products for given enterprise.
|
||||
# * Second step: render table and let user decide which ones to import.
|
||||
imported = catalog.products.map do |subject|
|
||||
imported = graph.map do |subject|
|
||||
next unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
||||
|
||||
existing_variant = enterprise.supplied_variants.linked_to(subject.semanticId)
|
||||
|
||||
if existing_variant
|
||||
@@ -41,5 +44,11 @@ module Admin
|
||||
flash[:error] = e.message
|
||||
redirect_to admin_product_import_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_catalog(url)
|
||||
DfcRequest.new(spree_current_user).call(url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,13 +30,13 @@ module Admin
|
||||
def validate_data
|
||||
return unless process_data('validate')
|
||||
|
||||
render json: @importer.import_results
|
||||
render json: @importer.import_results, response: 200
|
||||
end
|
||||
|
||||
def save_data
|
||||
return unless process_data('save')
|
||||
|
||||
render json: @importer.save_results
|
||||
render json: @importer.save_results, response: 200
|
||||
end
|
||||
|
||||
def reset_absent_products
|
||||
@@ -76,7 +76,7 @@ module Admin
|
||||
begin
|
||||
@importer.public_send("#{method}_entries")
|
||||
rescue StandardError => e
|
||||
render plain: e.message, status: :internal_server_error
|
||||
render json: e.message, response: 500
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ module Api
|
||||
end
|
||||
|
||||
def error_during_processing(exception)
|
||||
Alert.raise(exception)
|
||||
Bugsnag.notify(exception)
|
||||
|
||||
render(json: { exception: exception.message },
|
||||
status: :unprocessable_entity) && return
|
||||
|
||||
@@ -24,7 +24,6 @@ module Api
|
||||
Orders::WorkflowService.new(@order).advance_to_payment if @order.line_items.any?
|
||||
|
||||
@order.recreate_all_fees!
|
||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
||||
|
||||
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
||||
end
|
||||
@@ -74,7 +73,6 @@ module Api
|
||||
|
||||
@order.contents.add(variant, quantity, @shipment)
|
||||
@order.recreate_all_fees!
|
||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
||||
|
||||
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
||||
end
|
||||
@@ -88,7 +86,6 @@ module Api
|
||||
@shipment.reload if @shipment.persisted?
|
||||
|
||||
@order.recreate_all_fees!
|
||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
||||
|
||||
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
||||
end
|
||||
|
||||
@@ -52,7 +52,7 @@ module Api
|
||||
end
|
||||
|
||||
def error_during_processing(exception)
|
||||
Alert.raise(exception)
|
||||
Bugsnag.notify(exception)
|
||||
|
||||
if Rails.env.development? || Rails.env.test?
|
||||
render status: :unprocessable_entity,
|
||||
|
||||
@@ -11,7 +11,7 @@ class CartController < BaseController
|
||||
order.cap_quantity_at_stock!
|
||||
order.recreate_all_fees!
|
||||
|
||||
StockSyncJob.sync_linked_catalogs_later(order)
|
||||
StockSyncJob.sync_linked_catalogs(order)
|
||||
|
||||
render json: { error: false, stock_levels: stock_levels(order) }, status: :ok
|
||||
else
|
||||
|
||||
@@ -78,18 +78,6 @@ class CheckoutController < BaseController
|
||||
|
||||
return true if redirect_to_payment_gateway
|
||||
|
||||
# Redeem VINE voucher
|
||||
vine_voucher_redeemer = Vine::VoucherRedeemerService.new(order: @order)
|
||||
unless vine_voucher_redeemer.redeem
|
||||
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
|
||||
flash[:error] = if vine_voucher_redeemer.errors.keys.include?(:redeeming_failed)
|
||||
vine_voucher_redeemer.errors[:redeeming_failed]
|
||||
else
|
||||
I18n.t('checkout.errors.voucher_redeeming_error')
|
||||
end
|
||||
return false
|
||||
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
|
||||
end
|
||||
@order.process_payments!
|
||||
@order.confirm!
|
||||
order_completion_reset @order
|
||||
|
||||
@@ -50,7 +50,9 @@ module OrderCompletion
|
||||
end
|
||||
|
||||
def order_invalid!
|
||||
Alert.raise_with_record("Notice: invalid order loaded during checkout", @order)
|
||||
Bugsnag.notify("Notice: invalid order loaded during checkout") do |payload|
|
||||
payload.add_metadata :order, :order, @order
|
||||
end
|
||||
|
||||
flash[:error] = t('checkout.order_not_loaded')
|
||||
redirect_to main_app.shop_path
|
||||
@@ -90,7 +92,9 @@ module OrderCompletion
|
||||
end
|
||||
|
||||
def notify_failure(error = RuntimeError.new(order_processing_error))
|
||||
Alert.raise_with_record(error, @order)
|
||||
Bugsnag.notify(error) do |payload|
|
||||
payload.add_metadata :order, @order
|
||||
end
|
||||
flash[:error] = order_processing_error if flash.blank?
|
||||
end
|
||||
|
||||
|
||||
@@ -20,7 +20,9 @@ module OrderStockCheck
|
||||
def check_order_cycle_expiry
|
||||
return unless current_order_cycle&.closed?
|
||||
|
||||
Alert.raise_with_record("Notice: order cycle closed during checkout completion", current_order)
|
||||
Bugsnag.notify("Notice: order cycle closed during checkout completion") do |payload|
|
||||
payload.add_metadata :order, :order, current_order
|
||||
end
|
||||
current_order.empty!
|
||||
current_order.set_order_cycle! nil
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ class ErrorsController < ApplicationController
|
||||
layout "errors"
|
||||
|
||||
def not_found
|
||||
Bugsnag.notify("404") do |event|
|
||||
event.severity = "info"
|
||||
|
||||
event.add_metadata(:request, :env, request.env)
|
||||
end
|
||||
render status: :not_found, formats: :html
|
||||
end
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ module Spree
|
||||
@order.restock_items = params.fetch(:restock_items, "true") == "true"
|
||||
|
||||
if @order.public_send(event.to_s)
|
||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
||||
flash[:success] = Spree.t(:order_updated)
|
||||
else
|
||||
flash[:error] = Spree.t(:cannot_perform_operation)
|
||||
|
||||
@@ -24,12 +24,9 @@ module Spree
|
||||
end
|
||||
|
||||
def create
|
||||
# Try to redeem VINE voucher first as we don't want to create a payment and complete
|
||||
# the order if it fails
|
||||
return redirect_to spree.admin_order_payments_path(@order) unless redeem_vine_voucher
|
||||
|
||||
@payment = @order.payments.build(object_params)
|
||||
load_payment_source
|
||||
|
||||
begin
|
||||
unless @payment.save
|
||||
redirect_to spree.admin_order_payments_path(@order)
|
||||
@@ -54,10 +51,6 @@ module Spree
|
||||
event = params[:e]
|
||||
return unless event && @payment.payment_source
|
||||
|
||||
# capture_and_complete_order will complete the order, so we want to try to redeem VINE
|
||||
# voucher first and exit if it fails
|
||||
return if event == "capture_and_complete_order" && !redeem_vine_voucher
|
||||
|
||||
# Because we have a transition method also called void, we do this to avoid conflicts.
|
||||
event = "void_transaction" if event == "void"
|
||||
if allowed_events.include?(event) && @payment.public_send("#{event}!")
|
||||
@@ -67,7 +60,7 @@ module Spree
|
||||
end
|
||||
rescue StandardError => e
|
||||
logger.error e.message
|
||||
Alert.raise(e)
|
||||
Bugsnag.notify(e)
|
||||
flash[:error] = e.message
|
||||
ensure
|
||||
redirect_to request.referer
|
||||
@@ -189,22 +182,6 @@ module Spree
|
||||
%w{capture void_transaction credit refund resend_authorization_email
|
||||
capture_and_complete_order}
|
||||
end
|
||||
|
||||
def redeem_vine_voucher
|
||||
vine_voucher_redeemer = Vine::VoucherRedeemerService.new(order: @order)
|
||||
if vine_voucher_redeemer.redeem == false
|
||||
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
|
||||
flash[:error] = if vine_voucher_redeemer.errors.keys.include?(:redeeming_failed)
|
||||
vine_voucher_redeemer.errors[:redeeming_failed]
|
||||
else
|
||||
I18n.t('checkout.errors.voucher_redeeming_error')
|
||||
end
|
||||
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,6 @@ module Spree
|
||||
include OrderCyclesHelper
|
||||
include EnterprisesHelper
|
||||
helper ::Admin::ProductsHelper
|
||||
helper Spree::Admin::TaxCategoriesHelper
|
||||
|
||||
before_action :load_data
|
||||
before_action :load_producers, only: [:index, :new]
|
||||
@@ -214,7 +213,7 @@ module Spree
|
||||
end
|
||||
|
||||
def notify_bugsnag(error, product, variant)
|
||||
Alert.raise(error) do |report|
|
||||
Bugsnag.notify(error) do |report|
|
||||
report.add_metadata(:product,
|
||||
{ product: product.attributes, variant: variant.attributes })
|
||||
report.add_metadata(:product, :product_error, product.errors.first) unless product.valid?
|
||||
|
||||
@@ -70,15 +70,14 @@ module Spree
|
||||
@order.recreate_all_fees! # Enterprise fees on line items and on the order itself
|
||||
|
||||
# Re apply the voucher
|
||||
OrderManagement::Order::Updater.new(@order).update_voucher
|
||||
VoucherAdjustmentsService.new(@order).update
|
||||
@order.update_totals_and_states
|
||||
|
||||
if @order.complete?
|
||||
@order.update_payment_fees!
|
||||
@order.create_tax_charge!
|
||||
end
|
||||
|
||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
||||
|
||||
respond_with(@order) do |format|
|
||||
format.html do
|
||||
if params.key?(:checkout)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/error_logger'
|
||||
require "spree/core/controller_helpers/auth"
|
||||
require "spree/core/controller_helpers/common"
|
||||
require "spree/core/controller_helpers/order"
|
||||
@@ -36,7 +37,7 @@ class UserRegistrationsController < Devise::RegistrationsController
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
Alert.raise(e)
|
||||
OpenFoodNetwork::ErrorLogger.notify(e)
|
||||
render_error(message: I18n.t('unknown_error', scope: I18N_SCOPE))
|
||||
end
|
||||
|
||||
|
||||
@@ -4,16 +4,7 @@ class VoucherAdjustmentsController < BaseController
|
||||
before_action :set_order
|
||||
|
||||
def create
|
||||
if voucher_params[:voucher_code].blank?
|
||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
||||
return render_error
|
||||
end
|
||||
|
||||
voucher = load_voucher
|
||||
|
||||
return render_error unless valid_voucher?(voucher)
|
||||
|
||||
if add_voucher_to_order(voucher)
|
||||
if add_voucher
|
||||
update_payment_section
|
||||
elsif @order.errors.present?
|
||||
render_error
|
||||
@@ -39,28 +30,19 @@ class VoucherAdjustmentsController < BaseController
|
||||
@order = current_order
|
||||
end
|
||||
|
||||
def valid_voucher?(voucher)
|
||||
return false if @order.errors.present?
|
||||
def add_voucher
|
||||
if voucher_params[:voucher_code].blank?
|
||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
||||
return false
|
||||
end
|
||||
|
||||
voucher = Voucher.find_by(code: voucher_params[:voucher_code], enterprise: @order.distributor)
|
||||
|
||||
if voucher.nil?
|
||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
||||
return false
|
||||
end
|
||||
|
||||
if !voucher.valid?
|
||||
@order.errors.add(
|
||||
:voucher_code,
|
||||
I18n.t(
|
||||
'checkout.errors.create_voucher_error', error: voucher.errors.full_messages.to_sentence
|
||||
)
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def add_voucher_to_order(voucher)
|
||||
adjustment = voucher.create_adjustment(voucher.code, @order)
|
||||
|
||||
unless adjustment.persisted?
|
||||
@@ -69,38 +51,14 @@ class VoucherAdjustmentsController < BaseController
|
||||
return false
|
||||
end
|
||||
|
||||
# calculate_voucher_adjustment
|
||||
clear_payments
|
||||
|
||||
OrderManagement::Order::Updater.new(@order).update_voucher
|
||||
VoucherAdjustmentsService.new(@order).update
|
||||
@order.update_totals_and_states
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def load_voucher
|
||||
voucher = Voucher.find_by(code: voucher_params[:voucher_code],
|
||||
enterprise: @order.distributor)
|
||||
return voucher unless voucher.nil? || voucher.is_a?(Vouchers::Vine)
|
||||
|
||||
vine_voucher
|
||||
end
|
||||
|
||||
def vine_voucher
|
||||
vine_voucher_validator = Vine::VoucherValidatorService.new(
|
||||
voucher_code: voucher_params[:voucher_code], enterprise: @order.distributor
|
||||
)
|
||||
voucher = vine_voucher_validator.validate
|
||||
|
||||
return nil if vine_voucher_validator.errors[:not_found_voucher].present?
|
||||
|
||||
if vine_voucher_validator.errors.present?
|
||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.add_voucher_error'))
|
||||
return nil
|
||||
end
|
||||
|
||||
voucher
|
||||
end
|
||||
|
||||
def update_payment_section
|
||||
render cable_ready: cable_car.replace(
|
||||
selector: "#checkout-payment-methods",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
module Admin
|
||||
module TaxCategoriesHelper
|
||||
def tax_category_dropdown_options(require_tax_category)
|
||||
if require_tax_category
|
||||
{
|
||||
include_blank: false,
|
||||
selected: Spree::TaxCategory.find_by(is_default: true)&.id
|
||||
}
|
||||
else
|
||||
{
|
||||
include_blank: t(:none),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,30 +1,98 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# When orders are created, adjusted or cancelled, we need to amend
|
||||
# When orders are cancelled, we need to amend
|
||||
# an existing backorder as well.
|
||||
# We're not dealing with line item changes just yet.
|
||||
class AmendBackorderJob < ApplicationJob
|
||||
sidekiq_options retry: 0
|
||||
|
||||
def self.schedule_bulk_update_for(orders)
|
||||
# We can have one backorder per order cycle and distributor.
|
||||
groups = orders.group_by { |order| [order.order_cycle, order.distributor] }
|
||||
groups.each_value do |orders_with_same_backorder|
|
||||
# We need to trigger only one update per backorder.
|
||||
perform_later(orders_with_same_backorder.first)
|
||||
end
|
||||
end
|
||||
|
||||
def perform(order)
|
||||
OrderLocker.lock_order_and_variants(order) do
|
||||
amend_backorder(order)
|
||||
end
|
||||
end
|
||||
|
||||
# The following is a mix of the BackorderJob and the CompleteBackorderJob.
|
||||
# TODO: Move the common code into a re-usable service class.
|
||||
def amend_backorder(order)
|
||||
backorder = BackorderUpdater.new.amend_backorder(order)
|
||||
order_cycle = order.order_cycle
|
||||
distributor = order.distributor
|
||||
user = distributor.owner
|
||||
items = backorderable_items(order)
|
||||
|
||||
user = order.distributor.owner
|
||||
urls = nil # Not needed to send order. The backorder id is the URL.
|
||||
FdcBackorderer.new(user, urls).send_order(backorder) if backorder
|
||||
return if items.empty?
|
||||
|
||||
# We are assuming that all variants are linked to the same wholesale
|
||||
# shop and its catalog:
|
||||
reference_link = items[0].variant.semantic_links[0].semantic_id
|
||||
urls = FdcUrlBuilder.new(reference_link)
|
||||
orderer = FdcBackorderer.new(user, urls)
|
||||
|
||||
backorder = orderer.find_open_order(order)
|
||||
|
||||
variants = order_cycle.variants_distributed_by(distributor)
|
||||
adjust_quantities(order_cycle, user, backorder, urls, variants)
|
||||
|
||||
FdcBackorderer.new(user, urls).send_order(backorder)
|
||||
end
|
||||
|
||||
# Check if we have enough stock to reduce the backorder.
|
||||
#
|
||||
# Our local stock can increase when users cancel their orders.
|
||||
# But stock levels could also have been adjusted manually. So we review all
|
||||
# quantities before finalising the order.
|
||||
def adjust_quantities(order_cycle, user, order, urls, variants)
|
||||
broker = FdcOfferBroker.new(user, urls)
|
||||
|
||||
order.lines.each do |line|
|
||||
line.quantity = line.quantity.to_i
|
||||
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||
linked_variant = variants.linked_to(transformation.retail_product_id)
|
||||
|
||||
# Assumption: If a transformation is present then we only sell the retail
|
||||
# variant. If that can't be found, it was deleted and we'll ignore that
|
||||
# for now.
|
||||
next if linked_variant.nil?
|
||||
|
||||
# Find all line items for this order cycle
|
||||
# Update quantity accordingly
|
||||
if linked_variant.on_demand
|
||||
release_superfluous_stock(line, linked_variant, transformation)
|
||||
else
|
||||
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
|
||||
end
|
||||
end
|
||||
|
||||
# Clean up empty lines:
|
||||
order.lines.reject! { |line| line.quantity.zero? }
|
||||
end
|
||||
|
||||
# We look at all linked variants.
|
||||
def backorderable_items(order)
|
||||
order.line_items.select do |item|
|
||||
# TODO: scope variants to hub.
|
||||
# We are only supporting producer stock at the moment.
|
||||
item.variant.semantic_links.present?
|
||||
end
|
||||
end
|
||||
|
||||
def release_superfluous_stock(line, linked_variant, transformation)
|
||||
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
|
||||
|
||||
# But maybe we didn't actually order that much:
|
||||
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||
line.quantity -= deductable_quantity
|
||||
|
||||
retail_stock_changes = deductable_quantity * transformation.factor
|
||||
linked_variant.on_hand -= retail_stock_changes
|
||||
end
|
||||
|
||||
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
||||
orders = order_cycle.orders.invoiceable
|
||||
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
||||
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
||||
line.quantity = wholesale_quantity
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,7 +19,9 @@ class BackorderJob < ApplicationJob
|
||||
rescue StandardError => e
|
||||
# Errors here shouldn't affect the checkout. So let's report them
|
||||
# separately:
|
||||
Alert.raise_with_record(e, order)
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata(:order, :order, order)
|
||||
end
|
||||
end
|
||||
|
||||
def perform(order)
|
||||
@@ -117,8 +119,7 @@ class BackorderJob < ApplicationJob
|
||||
end
|
||||
|
||||
def load_broker(user, urls)
|
||||
catalog = DfcCatalog.load(user, urls.catalog_url)
|
||||
FdcOfferBroker.new(catalog)
|
||||
FdcOfferBroker.new(user, urls)
|
||||
end
|
||||
|
||||
def place_order(user, order, orderer, backorder)
|
||||
|
||||
@@ -24,7 +24,8 @@ class CompleteBackorderJob < ApplicationJob
|
||||
|
||||
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
|
||||
|
||||
BackorderUpdater.new.update(order, user, distributor, order_cycle)
|
||||
variants = order_cycle.variants_distributed_by(distributor)
|
||||
adjust_quantities(order_cycle, user, order, urls, variants)
|
||||
|
||||
FdcBackorderer.new(user, urls).complete_order(order)
|
||||
|
||||
@@ -35,4 +36,55 @@ class CompleteBackorderJob < ApplicationJob
|
||||
|
||||
raise
|
||||
end
|
||||
|
||||
# Check if we have enough stock to reduce the backorder.
|
||||
#
|
||||
# Our local stock can increase when users cancel their orders.
|
||||
# But stock levels could also have been adjusted manually. So we review all
|
||||
# quantities before finalising the order.
|
||||
def adjust_quantities(order_cycle, user, order, urls, variants)
|
||||
broker = FdcOfferBroker.new(user, urls)
|
||||
|
||||
order.lines.each do |line|
|
||||
line.quantity = line.quantity.to_i
|
||||
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||
linked_variant = variants.linked_to(transformation.retail_product_id)
|
||||
|
||||
# Assumption: If a transformation is present then we only sell the retail
|
||||
# variant. If that can't be found, it was deleted and we'll ignore that
|
||||
# for now.
|
||||
next if linked_variant.nil?
|
||||
|
||||
# Find all line items for this order cycle
|
||||
# Update quantity accordingly
|
||||
if linked_variant.on_demand
|
||||
release_superfluous_stock(line, linked_variant, transformation)
|
||||
else
|
||||
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
|
||||
end
|
||||
end
|
||||
|
||||
# Clean up empty lines:
|
||||
order.lines.reject! { |line| line.quantity.zero? }
|
||||
end
|
||||
|
||||
def release_superfluous_stock(line, linked_variant, transformation)
|
||||
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
|
||||
|
||||
# But maybe we didn't actually order that much:
|
||||
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||
line.quantity -= deductable_quantity
|
||||
|
||||
retail_stock_changes = deductable_quantity * transformation.factor
|
||||
linked_variant.on_hand -= retail_stock_changes
|
||||
end
|
||||
|
||||
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
||||
orders = order_cycle.orders.invoiceable
|
||||
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
||||
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
||||
line.quantity = wholesale_quantity
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,7 +22,11 @@ class ReportJob < ApplicationJob
|
||||
|
||||
broadcast_result(channel, format, blob) if channel
|
||||
rescue StandardError => e
|
||||
Alert.raise(e, { report: { report_class:, user:, params:, format: } })
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata :report, {
|
||||
report_class:, user:, params:, format:
|
||||
}
|
||||
end
|
||||
|
||||
broadcast_error(channel)
|
||||
end
|
||||
|
||||
@@ -8,12 +8,30 @@ class StockSyncJob < ApplicationJob
|
||||
# product. These variants are rare though and we check first before we
|
||||
# enqueue a new job. That should save some time loading the order with
|
||||
# all the stock data to make this decision.
|
||||
def self.sync_linked_catalogs_later(order)
|
||||
sync_catalogs_by_perform_method(order, :perform_later)
|
||||
def self.sync_linked_catalogs(order)
|
||||
user = order.distributor.owner
|
||||
catalog_ids(order).each do |catalog_id|
|
||||
perform_later(user, catalog_id)
|
||||
end
|
||||
rescue StandardError => e
|
||||
# Errors here shouldn't affect the shopping. So let's report them
|
||||
# separately:
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata(:order, :order, order)
|
||||
end
|
||||
end
|
||||
|
||||
def self.sync_linked_catalogs_now(order)
|
||||
sync_catalogs_by_perform_method(order, :perform_now)
|
||||
user = order.distributor.owner
|
||||
catalog_ids(order).each do |catalog_id|
|
||||
perform_now(user, catalog_id)
|
||||
end
|
||||
rescue StandardError => e
|
||||
# Errors here shouldn't affect the shopping. So let's report them
|
||||
# separately:
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata(:order, :order, order)
|
||||
end
|
||||
end
|
||||
|
||||
def self.catalog_ids(order)
|
||||
@@ -26,10 +44,7 @@ class StockSyncJob < ApplicationJob
|
||||
end
|
||||
|
||||
def perform(user, catalog_id)
|
||||
catalog = DfcCatalog.load(user, catalog_id)
|
||||
catalog.apply_wholesale_values!
|
||||
|
||||
products = catalog.products
|
||||
products = load_products(user, catalog_id)
|
||||
products_by_id = products.index_by(&:semanticId)
|
||||
product_ids = products_by_id.keys
|
||||
variants = linked_variants(user.enterprises, product_ids)
|
||||
@@ -47,23 +62,18 @@ class StockSyncJob < ApplicationJob
|
||||
end
|
||||
end
|
||||
|
||||
def load_products(user, catalog_id)
|
||||
json_catalog = DfcRequest.new(user).call(catalog_id)
|
||||
graph = DfcIo.import(json_catalog)
|
||||
|
||||
graph.select do |subject|
|
||||
subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
||||
end
|
||||
end
|
||||
|
||||
def linked_variants(enterprises, product_ids)
|
||||
Spree::Variant.where(supplier: enterprises)
|
||||
.includes(:semantic_links).references(:semantic_links)
|
||||
.where(semantic_links: { semantic_id: product_ids })
|
||||
end
|
||||
|
||||
def self.sync_catalogs_by_perform_method(order, perform_method)
|
||||
distributor = order.distributor
|
||||
return unless distributor
|
||||
|
||||
user = distributor.owner
|
||||
catalog_ids(order).each do |catalog_id|
|
||||
public_send(perform_method, user, catalog_id)
|
||||
end
|
||||
rescue StandardError => e
|
||||
# Errors here shouldn't affect the shopping. So let's report them
|
||||
# separately:
|
||||
Alert.raise_with_record(e, order)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,7 +55,9 @@ class SubscriptionConfirmJob < ApplicationJob
|
||||
if order.errors.any?
|
||||
send_failed_payment_email(order)
|
||||
else
|
||||
Alert.raise_with_record(e, order)
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata :order, :order, order
|
||||
end
|
||||
send_failed_payment_email(order, e.message)
|
||||
end
|
||||
end
|
||||
@@ -106,6 +108,8 @@ class SubscriptionConfirmJob < ApplicationJob
|
||||
record_and_log_error(:failed_payment, order, error_message)
|
||||
SubscriptionMailer.failed_payment_email(order).deliver_now
|
||||
rescue StandardError => e
|
||||
Alert.raise(e, { subscription_data: { order:, error_message: } })
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata :subscription_data, { order:, error_message: }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,19 +79,14 @@ class ProducerMailer < ApplicationMailer
|
||||
def set_customer_data(line_items)
|
||||
return unless @coordinator.show_customer_names_to_suppliers?
|
||||
|
||||
@display_business_name = false
|
||||
line_items.map do |line_item|
|
||||
customer_code = line_item.order.customer&.code
|
||||
@display_business_name = true if customer_code.present?
|
||||
|
||||
{
|
||||
sku: line_item.variant.sku,
|
||||
supplier_name: line_item.variant.supplier.name,
|
||||
product_and_full_name: line_item.product_and_full_name,
|
||||
quantity: line_item.quantity,
|
||||
first_name: line_item.order.billing_address.first_name,
|
||||
last_name: line_item.order.billing_address.last_name,
|
||||
business_name: customer_code,
|
||||
last_name: line_item.order.billing_address.last_name
|
||||
}
|
||||
end.sort_by { |line_item| [line_item[:last_name].downcase, line_item[:first_name].downcase] }
|
||||
end
|
||||
|
||||
@@ -28,7 +28,7 @@ module Calculator
|
||||
# In theory it should never be called any more after this has been deployed.
|
||||
# If the message below doesn't show up in Bugsnag, we can safely delete this method and all
|
||||
# the related methods below it.
|
||||
Alert.raise("Calculator::DefaultTax was called with legacy tax calculations")
|
||||
Bugsnag.notify("Calculator::DefaultTax was called with legacy tax calculations")
|
||||
|
||||
calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(order.distributor,
|
||||
order.order_cycle)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/concern"
|
||||
|
||||
module Vouchers
|
||||
module FlatRatable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
validates :amount,
|
||||
presence: true,
|
||||
numericality: { greater_than: 0 }
|
||||
end
|
||||
|
||||
def display_value
|
||||
Spree::Money.new(amount)
|
||||
end
|
||||
|
||||
# We limit adjustment to the maximum amount needed to cover the order, ie if the voucher
|
||||
# covers more than the order.total we only need to create an adjustment covering the order.total
|
||||
def compute_amount(order)
|
||||
-amount.clamp(0, order.pre_discount_total)
|
||||
end
|
||||
|
||||
def rate(order)
|
||||
amount = compute_amount(order)
|
||||
|
||||
amount / order.pre_discount_total
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -481,7 +481,7 @@ class Enterprise < ApplicationRecord
|
||||
|
||||
image_variant_url_for(image.variant(name))
|
||||
rescue StandardError => e
|
||||
Alert.raise "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
|
||||
Bugsnag.notify "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
|
||||
Rails.logger.error(e.message)
|
||||
|
||||
nil
|
||||
|
||||
@@ -79,9 +79,10 @@ module ProductImport
|
||||
if entry.attributes['on_hand'].present?
|
||||
new_variant.on_hand = entry.attributes['on_hand']
|
||||
end
|
||||
check_on_hand_nil(entry, new_variant)
|
||||
end
|
||||
|
||||
check_on_hand_nil(entry, new_variant)
|
||||
|
||||
if new_variant.valid?
|
||||
entry.product_object = new_variant
|
||||
entry.validates_as = 'new_variant' unless entry.errors?
|
||||
@@ -160,7 +161,7 @@ module ProductImport
|
||||
end
|
||||
|
||||
def unit_fields_validation(entry)
|
||||
unit_types = ['mg', 'g', 'kg', 'oz', 'lb', 't', 'ml', 'cl', 'dl', 'l', 'kl', 'gal', '']
|
||||
unit_types = ['g', 'oz', 'lb', 'kg', 't', 'ml', 'l', 'kl', '']
|
||||
|
||||
if entry.units.blank?
|
||||
mark_as_invalid(entry, attribute: 'units',
|
||||
@@ -296,7 +297,7 @@ module ProductImport
|
||||
unscaled_units = entry.unscaled_units.to_f || 0
|
||||
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
|
||||
|
||||
if entry.match_variant?(existing_variant)
|
||||
if entry.match_inventory_variant?(existing_variant)
|
||||
variant_override = create_inventory_item(entry, existing_variant)
|
||||
return validate_inventory_item(entry, variant_override)
|
||||
end
|
||||
|
||||
@@ -85,6 +85,10 @@ module ProductImport
|
||||
end
|
||||
|
||||
def match_variant?(variant)
|
||||
match_display_name?(variant) && variant.unit_value.to_d == unscaled_units.to_d
|
||||
end
|
||||
|
||||
def match_inventory_variant?(variant)
|
||||
match_display_name?(variant) && variant.unit_value.to_d == unit_value.to_d
|
||||
end
|
||||
|
||||
|
||||
@@ -32,18 +32,14 @@ module ProductImport
|
||||
|
||||
def unit_scales
|
||||
{
|
||||
'mg' => { scale: 0.001, unit: 'weight' },
|
||||
'g' => { scale: 1, unit: 'weight' },
|
||||
'kg' => { scale: 1000, unit: 'weight' },
|
||||
'oz' => { scale: 28.35, unit: 'weight' },
|
||||
'lb' => { scale: 453.6, unit: 'weight' },
|
||||
't' => { scale: 1_000_000, unit: 'weight' },
|
||||
'ml' => { scale: 0.001, unit: 'volume' },
|
||||
'cl' => { scale: 0.01, unit: 'volume' },
|
||||
'dl' => { scale: 0.1, unit: 'volume' },
|
||||
'l' => { scale: 1, unit: 'volume' },
|
||||
'kl' => { scale: 1000, unit: 'volume' },
|
||||
'gal' => { scale: 4.54609, unit: 'volume' },
|
||||
'kl' => { scale: 1000, unit: 'volume' }
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ module Spree
|
||||
|
||||
image_variant_url_for(variant(size))
|
||||
rescue StandardError => e
|
||||
Alert.raise "Product ##{viewable_id} Image ##{id} error: #{e.message}"
|
||||
Bugsnag.notify "Product ##{viewable_id} Image ##{id} error: #{e.message}"
|
||||
Rails.logger.error(e.message)
|
||||
|
||||
self.class.default_image_url(size)
|
||||
|
||||
@@ -535,7 +535,7 @@ module Spree
|
||||
# because an outdated shipping fee is not as bad as a lost payment.
|
||||
# And the shipping fee is already up-to-date when this error occurs.
|
||||
# https://github.com/openfoodfoundation/openfoodnetwork/issues/3924
|
||||
Alert.raise(e) do |report|
|
||||
Bugsnag.notify(e) do |report|
|
||||
report.add_metadata(:order, attributes)
|
||||
report.add_metadata(:shipment, shipment.attributes)
|
||||
report.add_metadata(:shipment_in_db, Spree::Shipment.find_by(id: shipment.id).attributes)
|
||||
|
||||
@@ -142,6 +142,8 @@ module Spree
|
||||
|
||||
OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
|
||||
update(payment_state: updater.update_payment_state)
|
||||
|
||||
AmendBackorderJob.perform_later(self)
|
||||
end
|
||||
|
||||
def after_resume
|
||||
|
||||
@@ -38,7 +38,6 @@ module Spree
|
||||
line_item.price = variant.price
|
||||
order.line_items << line_item
|
||||
end
|
||||
update_shipment
|
||||
|
||||
order.reload
|
||||
line_item
|
||||
|
||||
@@ -106,7 +106,7 @@ module Spree
|
||||
calculator.compute(item)
|
||||
else
|
||||
# Tax refund should not be possible with the way our production server are configured
|
||||
Alert.raise(
|
||||
Bugsnag.notify(
|
||||
"Notice: Tax refund should not be possible, please check the default zone and " \
|
||||
"the tax rate zone configuration"
|
||||
) do |payload|
|
||||
|
||||
@@ -293,7 +293,7 @@ module Spree
|
||||
end
|
||||
|
||||
def ensure_unit_value
|
||||
Alert.raise("Trying to set unit_value to NaN") if unit_value&.nan?
|
||||
Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan?
|
||||
return unless (variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
|
||||
|
||||
self.unit_value = 1.0
|
||||
|
||||
@@ -15,7 +15,7 @@ class StripeAccount < ApplicationRecord
|
||||
|
||||
destroy && Stripe::OAuth.deauthorize(stripe_user_id:)
|
||||
rescue Stripe::OAuth::OAuthError => e
|
||||
Alert.raise(
|
||||
Bugsnag.notify(
|
||||
e,
|
||||
stripe_account: stripe_user_id,
|
||||
enterprise_id:
|
||||
|
||||
@@ -48,8 +48,8 @@ class VariantOverride < ApplicationRecord
|
||||
|
||||
def move_stock!(quantity)
|
||||
unless stock_overridden?
|
||||
Alert.raise "Attempting to move stock of a VariantOverride " \
|
||||
"without a count_on_hand specified."
|
||||
Bugsnag.notify RuntimeError.new "Attempting to move stock of a VariantOverride " \
|
||||
"without a count_on_hand specified."
|
||||
return
|
||||
end
|
||||
|
||||
@@ -73,8 +73,8 @@ class VariantOverride < ApplicationRecord
|
||||
self.attributes = { on_demand: false, count_on_hand: default_stock }
|
||||
save
|
||||
else
|
||||
Alert.raise "Attempting to reset stock level for a variant " \
|
||||
"with no default stock level."
|
||||
Bugsnag.notify RuntimeError.new "Attempting to reset stock level for a variant " \
|
||||
"with no default stock level."
|
||||
end
|
||||
end
|
||||
self
|
||||
|
||||
@@ -14,7 +14,7 @@ class Voucher < ApplicationRecord
|
||||
class_name: 'Spree::Adjustment',
|
||||
dependent: nil
|
||||
|
||||
validates :code, presence: true
|
||||
validates :code, presence: true, uniqueness: { scope: :enterprise_id }
|
||||
|
||||
TYPES = ["Vouchers::FlatRate", "Vouchers::PercentageRate"].freeze
|
||||
|
||||
|
||||
@@ -2,8 +2,24 @@
|
||||
|
||||
module Vouchers
|
||||
class FlatRate < Voucher
|
||||
include FlatRatable
|
||||
validates :amount,
|
||||
presence: true,
|
||||
numericality: { greater_than: 0 }
|
||||
|
||||
validates_with ScopedUniquenessValidator
|
||||
def display_value
|
||||
Spree::Money.new(amount)
|
||||
end
|
||||
|
||||
# We limit adjustment to the maximum amount needed to cover the order, ie if the voucher
|
||||
# covers more than the order.total we only need to create an adjustment covering the order.total
|
||||
def compute_amount(order)
|
||||
-amount.clamp(0, order.pre_discount_total)
|
||||
end
|
||||
|
||||
def rate(order)
|
||||
amount = compute_amount(order)
|
||||
|
||||
amount / order.pre_discount_total
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,6 @@ module Vouchers
|
||||
validates :amount,
|
||||
presence: true,
|
||||
numericality: { greater_than: 0, less_than_or_equal_to: 100 }
|
||||
validates_with ScopedUniquenessValidator
|
||||
|
||||
def display_value
|
||||
ActionController::Base.helpers.number_to_percentage(amount, precision: 2)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
module Vouchers
|
||||
class Vine < Voucher
|
||||
include FlatRatable
|
||||
|
||||
# a VINE voucher :
|
||||
# - can potentially be associated with mutiple enterprise
|
||||
# - code ( "short code" in VINE ) can be recycled, but they shouldn't be linked to the same
|
||||
# voucher_id
|
||||
validates :code, uniqueness: { scope: [:enterprise_id, :external_voucher_id] }
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A handy wrapper around error reporting libraries like Bugsnag.
|
||||
#
|
||||
# Bugsnag's API is great for general purpose but overly complex for our use.
|
||||
# It also changes over time and we often make mistakes using it. So this class
|
||||
# aims at:
|
||||
#
|
||||
# * Abstracting from Bugsnag, open for other services.
|
||||
# * Simpler interface to reduce user error.
|
||||
# * Central place to update Bugsnag API usage when it changes.
|
||||
#
|
||||
class Alert
|
||||
# Alert Bugsnag with additional metadata to appear in tabs.
|
||||
#
|
||||
# Alert.raise(
|
||||
# "Invalid order during checkout",
|
||||
# {
|
||||
# order: { number: "ABC123", state: "awaiting_return" },
|
||||
# env: { referer: "example.com" }
|
||||
# }
|
||||
# )
|
||||
def self.raise(error, metadata = {}, &block)
|
||||
Bugsnag.notify(error) do |payload|
|
||||
metadata.each do |name, data|
|
||||
payload.add_metadata(name, data)
|
||||
end
|
||||
block.call(payload)
|
||||
end
|
||||
end
|
||||
|
||||
def self.raise_with_record(error, record, &)
|
||||
metadata = {
|
||||
record.class.name => record&.attributes || { record_was_nil: true }
|
||||
}
|
||||
self.raise(error, metadata, &)
|
||||
end
|
||||
end
|
||||
@@ -1,148 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/order_cycle_permissions'
|
||||
|
||||
# Update a backorder to reflect all local orders and stock levels
|
||||
# connected to the associated order cycle.
|
||||
class BackorderUpdater
|
||||
# Given an OFN order was created, changed or cancelled,
|
||||
# we re-calculate how much to order in for every variant.
|
||||
def amend_backorder(order)
|
||||
order_cycle = order.order_cycle
|
||||
distributor = order.distributor
|
||||
variants = distributed_linked_variants(order_cycle, distributor)
|
||||
|
||||
# Temporary code: once we don't need a variant link to look up the
|
||||
# backorder, we don't need this check anymore.
|
||||
# Then we can adjust the backorder even though there are no linked variants
|
||||
# in the order cycle right now. Some variants may have been in the order
|
||||
# cycle before and got ordered before being removed from the order cycle.
|
||||
return unless variants.any?
|
||||
|
||||
# We are assuming that all variants are linked to the same wholesale
|
||||
# shop and its catalog:
|
||||
reference_link = variants[0].semantic_links[0].semantic_id
|
||||
user = order.distributor.owner
|
||||
urls = FdcUrlBuilder.new(reference_link)
|
||||
orderer = FdcBackorderer.new(user, urls)
|
||||
|
||||
backorder = orderer.find_open_order(order)
|
||||
|
||||
update(backorder, user, distributor, order_cycle)
|
||||
end
|
||||
|
||||
# Update a given backorder according to a distributor's order cycle.
|
||||
def update(backorder, user, distributor, order_cycle)
|
||||
variants = distributed_linked_variants(order_cycle, distributor)
|
||||
|
||||
# We are assuming that all variants are linked to the same wholesale
|
||||
# shop and its catalog:
|
||||
reference_link = variants[0].semantic_links[0].semantic_id
|
||||
urls = FdcUrlBuilder.new(reference_link)
|
||||
orderer = FdcBackorderer.new(user, urls)
|
||||
catalog = DfcCatalog.load(user, urls.catalog_url)
|
||||
broker = FdcOfferBroker.new(catalog)
|
||||
|
||||
updated_lines = update_order_lines(backorder, order_cycle, variants, broker, orderer)
|
||||
unprocessed_lines = backorder.lines.to_set - updated_lines
|
||||
managed_variants = managed_linked_variants(user, order_cycle, distributor)
|
||||
cancel_stale_lines(unprocessed_lines, managed_variants, broker)
|
||||
|
||||
# Clean up empty lines:
|
||||
backorder.lines.reject! { |line| line.quantity.zero? }
|
||||
|
||||
backorder
|
||||
end
|
||||
|
||||
def update_order_lines(backorder, order_cycle, variants, broker, orderer)
|
||||
variants.map do |variant|
|
||||
link = variant.semantic_links[0].semantic_id
|
||||
solution = broker.best_offer(link)
|
||||
line = orderer.find_or_build_order_line(backorder, solution.offer)
|
||||
if variant.on_demand
|
||||
adjust_stock(variant, solution, line)
|
||||
else
|
||||
aggregate_final_quantities(order_cycle, line, variant, solution)
|
||||
end
|
||||
|
||||
line
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_stale_lines(unprocessed_lines, managed_variants, broker)
|
||||
unprocessed_lines.each do |line|
|
||||
wholesale_quantity = line.quantity.to_i
|
||||
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||
linked_variant = managed_variants.linked_to(transformation.retail_product_id)
|
||||
|
||||
if linked_variant.nil?
|
||||
transformation.factor = 1
|
||||
linked_variant = managed_variants.linked_to(wholesale_product_id)
|
||||
end
|
||||
|
||||
# Adjust stock level back, we're not going to order this one.
|
||||
if linked_variant&.on_demand
|
||||
retail_quantity = wholesale_quantity * transformation.factor
|
||||
linked_variant.on_hand -= retail_quantity
|
||||
end
|
||||
|
||||
# We don't have any active orders for this
|
||||
line.quantity = 0
|
||||
end
|
||||
end
|
||||
|
||||
def adjust_stock(variant, solution, line)
|
||||
line.quantity = line.quantity.to_i
|
||||
|
||||
if variant.on_hand.negative?
|
||||
needed_quantity = -1 * variant.on_hand # We need to replenish it.
|
||||
|
||||
# The number of wholesale packs we need to order to fulfill the
|
||||
# needed quantity.
|
||||
# For example, we order 2 packs of 12 cans if we need 15 cans.
|
||||
wholesale_quantity = (needed_quantity.to_f / solution.factor).ceil
|
||||
|
||||
# The number of individual retail items we get with the wholesale order.
|
||||
# For example, if we order 2 packs of 12 cans, we will get 24 cans
|
||||
# and we'll account for that in our stock levels.
|
||||
retail_quantity = wholesale_quantity * solution.factor
|
||||
|
||||
line.quantity += wholesale_quantity
|
||||
variant.on_hand += retail_quantity
|
||||
else
|
||||
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||
wholesale_items_contained_in_stock = variant.on_hand / solution.factor
|
||||
|
||||
# But maybe we didn't actually order that much:
|
||||
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||
|
||||
if deductable_quantity.positive?
|
||||
line.quantity -= deductable_quantity
|
||||
|
||||
retail_stock_change = deductable_quantity * solution.factor
|
||||
variant.on_hand -= retail_stock_change
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def managed_linked_variants(user, order_cycle, distributor)
|
||||
# These permissions may be too complex. Here may be scope to optimise.
|
||||
permissions = OpenFoodNetwork::OrderCyclePermissions.new(user, order_cycle)
|
||||
permissions.visible_variants_for_outgoing_exchanges_to(distributor)
|
||||
.where.associated(:semantic_links)
|
||||
end
|
||||
|
||||
def distributed_linked_variants(order_cycle, distributor)
|
||||
order_cycle.variants_distributed_by(distributor)
|
||||
.where.associated(:semantic_links)
|
||||
end
|
||||
|
||||
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
||||
# We may want to query all these quantities in one go instead of this n+1.
|
||||
orders = order_cycle.orders.invoiceable
|
||||
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
||||
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
||||
line.quantity = wholesale_quantity
|
||||
end
|
||||
end
|
||||
@@ -21,7 +21,7 @@ class FdcBackorderer
|
||||
|
||||
# Try the new method and fall back to old method.
|
||||
def find_open_order(ofn_order)
|
||||
lookup_open_order(ofn_order)
|
||||
lookup_open_order(ofn_order) || find_last_open_order
|
||||
end
|
||||
|
||||
def lookup_open_order(ofn_order)
|
||||
@@ -36,13 +36,52 @@ class FdcBackorderer
|
||||
.map { |id| find_order(id) }
|
||||
.compact
|
||||
# Just in case someone completed the order without updating our database:
|
||||
.select { |o| o.orderStatus == order_status.HELD }
|
||||
.select { |o| o.orderStatus[:path] == "Held" }
|
||||
.first
|
||||
# The DFC Connector doesn't recognise status values properly yet.
|
||||
# So we are overriding the value with something that can be exported.
|
||||
&.tap { |o| o.orderStatus = "dfc-v:Held" }
|
||||
end
|
||||
|
||||
# DEPRECATED
|
||||
#
|
||||
# We now store links to orders we placed. So we don't need to search
|
||||
# through all orders and pick a random open one.
|
||||
# But for compatibility with currently open order cycles that don't have
|
||||
# a stored link yet, we keep this method as well.
|
||||
def find_last_open_order
|
||||
graph = import(urls.orders_url)
|
||||
open_orders = graph&.select do |o|
|
||||
o.semanticType == "dfc-b:Order" && o.orderStatus[:path] == "Held"
|
||||
end
|
||||
|
||||
return if open_orders.blank?
|
||||
|
||||
# If there are multiple open orders, we don't know which one to choose.
|
||||
# We want the order we placed for the same distributor in the same order
|
||||
# cycle before. So here are some assumptions for this to work:
|
||||
#
|
||||
# * We see only orders for our distributor. The endpoint URL contains the
|
||||
# the distributor name and is currently hardcoded.
|
||||
# * There's only one open order cycle at a time. Otherwise we may select
|
||||
# an order of an old order cycle.
|
||||
# * Orders are finalised when the order cycle closes. So _Held_ orders
|
||||
# always belong to an open order cycle.
|
||||
# * We see only our own orders. This assumption is wrong. The Shopify
|
||||
# integration places held orders as well and they are visible to us.
|
||||
#
|
||||
# Unfortunately, the endpoint doesn't tell who placed the order.
|
||||
# TODO: We need to remember the link to the order locally.
|
||||
# Or the API is updated to include the orderer.
|
||||
#
|
||||
# For now, we just guess:
|
||||
open_orders.last.tap do |order|
|
||||
# The DFC Connector doesn't recognise status values properly yet.
|
||||
# So we are overriding the value with something that can be exported.
|
||||
order.orderStatus = "dfc-v:Held"
|
||||
end
|
||||
end
|
||||
|
||||
def find_order(semantic_id)
|
||||
find_subject(import(semantic_id), "dfc-b:Order")
|
||||
end
|
||||
@@ -113,7 +152,7 @@ class FdcBackorderer
|
||||
end
|
||||
|
||||
def new?(order)
|
||||
order.semanticId == urls&.orders_url
|
||||
order.semanticId == urls.orders_url
|
||||
end
|
||||
|
||||
def build_sale_session(order)
|
||||
@@ -121,10 +160,4 @@ class FdcBackorderer
|
||||
session.semanticId = urls.sale_session_url
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def order_status
|
||||
DfcLoader.vocabulary("vocabulary").STATES.ORDERSTATE
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,10 +6,19 @@ class FdcOfferBroker
|
||||
Solution = Struct.new(:product, :factor, :offer)
|
||||
RetailSolution = Struct.new(:retail_product_id, :factor)
|
||||
|
||||
attr_reader :catalog
|
||||
def self.load_catalog(user, urls)
|
||||
api = DfcRequest.new(user)
|
||||
catalog_json = api.call(urls.catalog_url)
|
||||
DfcIo.import(catalog_json)
|
||||
end
|
||||
|
||||
def initialize(catalog)
|
||||
@catalog = catalog
|
||||
def initialize(user, urls)
|
||||
@user = user
|
||||
@urls = urls
|
||||
end
|
||||
|
||||
def catalog
|
||||
@catalog ||= self.class.load_catalog(@user, @urls)
|
||||
end
|
||||
|
||||
def best_offer(product_id)
|
||||
@@ -21,18 +30,19 @@ class FdcOfferBroker
|
||||
end
|
||||
|
||||
def wholesale_product(product_id)
|
||||
production_flow = catalog.item("#{product_id}/AsPlannedProductionFlow")
|
||||
production_flow = catalog_item("#{product_id}/AsPlannedProductionFlow")
|
||||
|
||||
if production_flow
|
||||
production_flow.product
|
||||
wholesale_product_id = production_flow.product
|
||||
catalog_item(wholesale_product_id)
|
||||
else
|
||||
# We didn't find a wholesale variant, falling back to the given product.
|
||||
catalog.item(product_id)
|
||||
catalog_item(product_id)
|
||||
end
|
||||
end
|
||||
|
||||
def contained_quantity(product_id)
|
||||
consumption_flow = catalog.item("#{product_id}/AsPlannedConsumptionFlow")
|
||||
consumption_flow = catalog_item("#{product_id}/AsPlannedConsumptionFlow")
|
||||
|
||||
# If we don't find a transformation, we return the original product,
|
||||
# which contains exactly one of itself (identity).
|
||||
@@ -44,10 +54,10 @@ class FdcOfferBroker
|
||||
|
||||
return RetailSolution.new(wholesale_product_id, 1) if production_flow.nil?
|
||||
|
||||
consumption_flow = catalog.item(
|
||||
consumption_flow = catalog_item(
|
||||
production_flow.semanticId.sub("AsPlannedProductionFlow", "AsPlannedConsumptionFlow")
|
||||
)
|
||||
retail_product_id = consumption_flow.product.semanticId
|
||||
retail_product_id = consumption_flow.product
|
||||
|
||||
contained_quantity = consumption_flow.quantity.value.to_i
|
||||
|
||||
@@ -61,12 +71,19 @@ class FdcOfferBroker
|
||||
end
|
||||
end
|
||||
|
||||
def catalog_item(id)
|
||||
@catalog_by_id ||= catalog.index_by(&:semanticId)
|
||||
@catalog_by_id[id]
|
||||
end
|
||||
|
||||
def flow_producing(wholesale_product_id)
|
||||
@production_flows_by_product_id ||= production_flows.index_by { |flow| flow.product.semanticId }
|
||||
@production_flows_by_product_id ||= production_flows.index_by(&:product)
|
||||
@production_flows_by_product_id[wholesale_product_id]
|
||||
end
|
||||
|
||||
def production_flows
|
||||
@production_flows ||= catalog.select_type("dfc-b:AsPlannedProductionFlow")
|
||||
@production_flows ||= catalog.select do |i|
|
||||
i.semanticType == "dfc-b:AsPlannedProductionFlow"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ module Orders
|
||||
order.send_cancellation_email = @send_cancellation_email
|
||||
order.restock_items = @restock_items
|
||||
order.cancel
|
||||
end.tap { |orders| AmendBackorderJob.schedule_bulk_update_for(orders) }
|
||||
end
|
||||
# rubocop:enable Rails/FindEach
|
||||
end
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ module Orders
|
||||
return unless order.cancel
|
||||
|
||||
Spree::OrderMailer.cancel_email_for_shop(order).deliver_later
|
||||
AmendBackorderJob.perform_later(order)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -24,7 +24,9 @@ class PlaceProxyOrder
|
||||
send_placement_email
|
||||
rescue StandardError => e
|
||||
summarizer.record_and_log_error(:processing, order, e.message)
|
||||
Alert.raise_with_record(e, order)
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata :order, :order, order
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -54,7 +56,9 @@ class PlaceProxyOrder
|
||||
|
||||
true
|
||||
rescue StandardError => e
|
||||
Alert.raise(e, { proxy_order: { subscription:, proxy_order: } })
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata(:proxy_order, { subscription:, proxy_order: })
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ module Sets
|
||||
end
|
||||
|
||||
def notify_bugsnag(error, product, variant, variant_attributes)
|
||||
Alert.raise(error) do |report|
|
||||
Bugsnag.notify(error) do |report|
|
||||
report.add_metadata( :product_set,
|
||||
{ product: product.attributes, variant_attributes:,
|
||||
variant: variant.attributes } )
|
||||
|
||||
@@ -58,7 +58,7 @@ module VariantUnits
|
||||
def option_value_value_unit_scaled
|
||||
unit_scale, unit_name = scale_for_unit_value
|
||||
|
||||
value = (@nameable.unit_value.to_d / unit_scale).round(2)
|
||||
value = (@nameable.unit_value / unit_scale).to_d.truncate(2)
|
||||
|
||||
[value, unit_name]
|
||||
end
|
||||
|
||||
@@ -65,7 +65,6 @@ module VariantUnits
|
||||
def unit_value_attributes
|
||||
units = { unit_presentation: option_value_name }
|
||||
units.merge!(variant_unit:) if has_attribute?(:variant_unit)
|
||||
units.merge!(variant_unit_name: '') if reset_variant_unit_name?
|
||||
units
|
||||
end
|
||||
|
||||
@@ -80,9 +79,5 @@ module VariantUnits
|
||||
|
||||
VariantUnits::OptionValueNamer.new(self).name
|
||||
end
|
||||
|
||||
def reset_variant_unit_name?
|
||||
has_attribute?(:variant_unit_name) && has_attribute?(:variant_unit) && variant_unit != 'items'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "faraday"
|
||||
|
||||
module Vine
|
||||
class ApiService
|
||||
attr_reader :api_key, :jwt_generator
|
||||
|
||||
def initialize(api_key:, jwt_generator:)
|
||||
@vine_api_url = ENV.fetch("VINE_API_URL")
|
||||
@api_key = api_key
|
||||
@jwt_generator = jwt_generator
|
||||
end
|
||||
|
||||
def my_team
|
||||
my_team_url = "#{@vine_api_url}/my-team"
|
||||
|
||||
call_with_logging do
|
||||
connection.get(my_team_url)
|
||||
end
|
||||
end
|
||||
|
||||
def voucher_validation(voucher_short_code)
|
||||
voucher_validation_url = "#{@vine_api_url}/voucher-validation"
|
||||
|
||||
call_with_logging do
|
||||
connection.post(
|
||||
voucher_validation_url,
|
||||
{ type: "voucher_code", value: voucher_short_code },
|
||||
'Content-Type': "application/json"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def voucher_redemptions(voucher_id, voucher_set_id, amount)
|
||||
voucher_redemptions_url = "#{@vine_api_url}/voucher-redemptions"
|
||||
|
||||
call_with_logging do
|
||||
connection.post(
|
||||
voucher_redemptions_url,
|
||||
{ voucher_id:, voucher_set_id:, amount: amount.to_i },
|
||||
'Content-Type': "application/json"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def connection
|
||||
jwt = jwt_generator.generate_token
|
||||
Faraday.new(
|
||||
request: { timeout: 30 },
|
||||
headers: {
|
||||
'X-Authorization': "JWT #{jwt}",
|
||||
Accept: "application/json"
|
||||
}
|
||||
) do |f|
|
||||
f.request :json
|
||||
f.response :json
|
||||
f.request :authorization, 'Bearer', api_key
|
||||
f.use Faraday::Response::RaiseError
|
||||
end
|
||||
end
|
||||
|
||||
def call_with_logging
|
||||
yield
|
||||
rescue Faraday::ClientError, Faraday::ServerError => e
|
||||
# caller_location(2,1) gets us the second entry in the stacktrace,
|
||||
# ie the method where `call_with_logging` is called from
|
||||
log_error("#{self.class}##{caller_locations(2, 1)[0].label}", e.response)
|
||||
|
||||
# Re raise the same exception
|
||||
raise
|
||||
end
|
||||
|
||||
def log_error(prefix, response)
|
||||
Rails.logger.error "#{prefix} -- response_status: #{response[:status]}"
|
||||
Rails.logger.error "#{prefix} -- response: #{response[:body]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vine
|
||||
class JwtService
|
||||
ALGORITHM = "HS256"
|
||||
ISSUER = "openfoodnetwork"
|
||||
|
||||
def initialize(secret: )
|
||||
@secret = secret
|
||||
end
|
||||
|
||||
def generate_token
|
||||
generation_time = Time.zone.now
|
||||
payload = {
|
||||
iss: ISSUER,
|
||||
iat: generation_time.to_i,
|
||||
exp: (generation_time + 1.minute).to_i,
|
||||
}
|
||||
|
||||
JWT.encode(payload, @secret, ALGORITHM)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vine
|
||||
class VoucherRedeemerService
|
||||
attr_reader :order, :errors
|
||||
|
||||
def initialize(order: )
|
||||
@order = order
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def redeem
|
||||
# Do nothing if we don't have a vine voucher added to the order
|
||||
@voucher_adjustment = order.voucher_adjustments.first
|
||||
@voucher = @voucher_adjustment&.originator
|
||||
|
||||
return true if @voucher_adjustment.nil? || !@voucher.is_a?(Vouchers::Vine)
|
||||
|
||||
return false if vine_settings.nil?
|
||||
|
||||
call_vine_api
|
||||
|
||||
@voucher_adjustment.close
|
||||
|
||||
true
|
||||
rescue Faraday::ClientError => e
|
||||
handle_errors(e.response)
|
||||
false
|
||||
rescue Faraday::Error => e
|
||||
Rails.logger.error e.inspect
|
||||
Bugsnag.notify(e)
|
||||
|
||||
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def vine_settings
|
||||
@vine_settings ||= ConnectedApps::Vine.find_by(enterprise: order.distributor)&.data
|
||||
end
|
||||
|
||||
def call_vine_api
|
||||
jwt_service = Vine::JwtService.new(secret: vine_settings["secret"])
|
||||
vine_api = Vine::ApiService.new(api_key: vine_settings["api_key"], jwt_generator: jwt_service)
|
||||
|
||||
# Voucher adjustment amount is stored in dollars and negative, VINE expect cents
|
||||
amount = -1 * @voucher_adjustment.amount * 100
|
||||
vine_api.voucher_redemptions(
|
||||
@voucher.external_voucher_id, @voucher.external_voucher_set_id, amount
|
||||
)
|
||||
end
|
||||
|
||||
def handle_errors(response)
|
||||
if response[:status] == 400
|
||||
errors[:redeeming_failed] = I18n.t("vine_voucher_redeemer_service.errors.redeeming_failed")
|
||||
else
|
||||
errors[:vine_api] = I18n.t("vine_voucher_redeemer_service.errors.vine_api")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,71 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Vine
|
||||
class VoucherValidatorService
|
||||
attr_reader :voucher_code, :errors
|
||||
|
||||
def initialize(voucher_code:, enterprise:)
|
||||
@voucher_code = voucher_code
|
||||
@enterprise = enterprise
|
||||
@errors = {}
|
||||
end
|
||||
|
||||
def validate
|
||||
return nil if vine_settings.nil?
|
||||
|
||||
response = call_vine_api
|
||||
|
||||
save_voucher(response)
|
||||
rescue Faraday::ClientError => e
|
||||
handle_errors(e.response)
|
||||
nil
|
||||
rescue Faraday::Error => e
|
||||
Rails.logger.error e.inspect
|
||||
Bugsnag.notify(e)
|
||||
|
||||
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def vine_settings
|
||||
@vine_settings ||= ConnectedApps::Vine.find_by(enterprise: @enterprise)&.data
|
||||
end
|
||||
|
||||
def call_vine_api
|
||||
# Check voucher is valid
|
||||
jwt_service = Vine::JwtService.new(secret: vine_settings["secret"])
|
||||
vine_api = Vine::ApiService.new(api_key: vine_settings["api_key"], jwt_generator: jwt_service)
|
||||
|
||||
vine_api.voucher_validation(voucher_code)
|
||||
end
|
||||
|
||||
def handle_errors(response)
|
||||
if response[:status] == 400
|
||||
errors[:invalid_voucher] = I18n.t("vine_voucher_validator_service.errors.invalid_voucher")
|
||||
elsif response[:status] == 404
|
||||
errors[:not_found_voucher] =
|
||||
I18n.t("vine_voucher_validator_service.errors.not_found_voucher")
|
||||
else
|
||||
errors[:vine_api] = I18n.t("vine_voucher_validator_service.errors.vine_api")
|
||||
end
|
||||
end
|
||||
|
||||
def save_voucher(response)
|
||||
voucher_data = response.body["data"]
|
||||
|
||||
# Check if voucher already exist
|
||||
voucher = Vouchers::Vine.find_or_initialize_by(
|
||||
code: voucher_code,
|
||||
enterprise: @enterprise,
|
||||
external_voucher_id: voucher_data["id"],
|
||||
external_voucher_set_id: voucher_data["voucher_set_id"]
|
||||
)
|
||||
voucher.amount = voucher_data["voucher_value_remaining"].to_f / 100
|
||||
voucher.save
|
||||
|
||||
voucher
|
||||
end
|
||||
end
|
||||
end
|
||||
39
app/services/vine_api_service.rb
Normal file
39
app/services/vine_api_service.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "faraday"
|
||||
|
||||
class VineApiService
|
||||
attr_reader :api_key, :jwt_generator
|
||||
|
||||
def initialize(api_key:, jwt_generator:)
|
||||
@vine_api_url = ENV.fetch("VINE_API_URL")
|
||||
@api_key = api_key
|
||||
@jwt_generator = jwt_generator
|
||||
end
|
||||
|
||||
def my_team
|
||||
my_team_url = "#{@vine_api_url}/my-team"
|
||||
|
||||
jwt = jwt_generator.generate_token
|
||||
connection = Faraday.new(
|
||||
request: { timeout: 30 },
|
||||
headers: {
|
||||
'X-Authorization': "JWT #{jwt}",
|
||||
Accept: "application/json"
|
||||
}
|
||||
) do |f|
|
||||
f.request :json
|
||||
f.response :json
|
||||
f.request :authorization, 'Bearer', api_key
|
||||
end
|
||||
|
||||
response = connection.get(my_team_url)
|
||||
|
||||
if !response.success?
|
||||
Rails.logger.error "VineApiService#my_team -- response_status: #{response.status}"
|
||||
Rails.logger.error "VineApiService#my_team -- response: #{response.body}"
|
||||
end
|
||||
|
||||
response
|
||||
end
|
||||
end
|
||||
21
app/services/vine_jwt_service.rb
Normal file
21
app/services/vine_jwt_service.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VineJwtService
|
||||
ALGORITHM = "HS256"
|
||||
ISSUER = "openfoodnetwork"
|
||||
|
||||
def initialize(secret: )
|
||||
@secret = secret
|
||||
end
|
||||
|
||||
def generate_token
|
||||
generation_time = Time.zone.now
|
||||
payload = {
|
||||
iss: ISSUER,
|
||||
iat: generation_time.to_i,
|
||||
exp: (generation_time + 1.minute).to_i,
|
||||
}
|
||||
|
||||
JWT.encode(payload, @secret, ALGORITHM)
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
# paranoia doesn't support unique validation including deleted records:
|
||||
# https://github.com/rubysherpas/paranoia/pull/333
|
||||
# We use a custom validator to fix the issue, so we don't need to fork/patch the gem
|
||||
module Vouchers
|
||||
class ScopedUniquenessValidator < ActiveModel::Validator
|
||||
def validate(record)
|
||||
@record = record
|
||||
|
||||
return unless unique_voucher_code_per_enterprise?
|
||||
|
||||
record.errors.add :code, :taken, value: @record.code
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unique_voucher_code_per_enterprise?
|
||||
query = Voucher.with_deleted.where(code: @record.code, enterprise_id: @record.enterprise_id)
|
||||
query = query.where.not(id: @record.id) unless @record.id.nil?
|
||||
|
||||
query.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
= t('.add_new')
|
||||
%br
|
||||
|
||||
- if @enterprise.vouchers.where.not(type: "Vouchers::Vine").with_deleted.present?
|
||||
- if @enterprise.vouchers.with_deleted.present?
|
||||
%table
|
||||
%thead
|
||||
%tr
|
||||
@@ -17,7 +17,7 @@
|
||||
/%th= t('.customers')
|
||||
/%th= t('.net_value')
|
||||
%tbody
|
||||
- @enterprise.vouchers.where.not(type: "Vouchers::Vine").with_deleted.order(deleted_at: :desc, code: :asc).each do |voucher|
|
||||
- @enterprise.vouchers.with_deleted.order(deleted_at: :desc, code: :asc).each do |voucher|
|
||||
%tr
|
||||
%td= voucher.code
|
||||
%td= voucher.display_value
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
%em= t(".need_to_be_manager") unless managed_by_user?(enterprise)
|
||||
- else
|
||||
= button_to t(".disable"), admin_enterprise_connected_app_path(connected_app.id, enterprise_id: enterprise.id), method: :delete
|
||||
|
||||
%hr
|
||||
.connected-app__description
|
||||
%p= t ".description"
|
||||
%b= link_to(t(".link_label_html"), t(".link_url"), target: "_blank")
|
||||
= t ".description_html"
|
||||
|
||||
@@ -27,5 +27,4 @@
|
||||
|
||||
%hr
|
||||
.connected-app__description
|
||||
%p= t ".description"
|
||||
%b= link_to(t(".link_label_html"), t(".link_url"), target: "_blank")
|
||||
= t ".description_html"
|
||||
|
||||
@@ -21,12 +21,10 @@
|
||||
|
||||
-# This is only seen by super-admins:
|
||||
%em= t(".need_to_be_manager") unless managed_by_user?(enterprise)
|
||||
%hr
|
||||
.connected-app__description
|
||||
= t ".description"
|
||||
- else
|
||||
.connected-app__vine-content
|
||||
.vine-disable
|
||||
= button_to t(".disable"), admin_enterprise_connected_app_path(connected_app.id, enterprise_id: enterprise.id), method: :delete
|
||||
.connected-app__vine-link
|
||||
%b= link_to(t(".link_label_html"), t(".link_url"), target: "_blank")
|
||||
%hr
|
||||
.connected-app__description
|
||||
= t ".description_html"
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
.container
|
||||
.sixteen.columns
|
||||
= render partial: 'admin/shared/flashes', locals: { flashes: } if defined? flashes
|
||||
= render partial: 'filters', locals: { search_term:,
|
||||
producer_id:,
|
||||
producer_options:,
|
||||
category_options:,
|
||||
category_id: } if display_search_filter
|
||||
= render partial: 'filters', locals: { search_term: search_term,
|
||||
producer_id: producer_id,
|
||||
producer_options: producer_options,
|
||||
category_options: category_options,
|
||||
category_id: category_id }
|
||||
- if products.any?
|
||||
.container.results
|
||||
.sixteen.columns
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
= f.text_field :name, 'aria-label': t('admin.products_page.columns.name')
|
||||
= error_message_on product, :name
|
||||
%td.col-sku.field.naked_inputs
|
||||
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
|
||||
= error_message_on product, :sku
|
||||
%td.col-unit_scale.align-right
|
||||
-# empty
|
||||
%td.col-unit.align-right
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
= f.hidden_field :variant_unit_scale
|
||||
= f.select :variant_unit_with_scale,
|
||||
options_for_select(WeightsAndMeasures.variant_unit_options, variant.variant_unit_with_scale),
|
||||
{ include_blank: t('.select_unit_scale') },
|
||||
{ include_blank: true },
|
||||
{ class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" }, required: true }
|
||||
= error_message_on variant, :variant_unit, 'data-toggle-control-target': 'control'
|
||||
.field
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
= render partial: "content", locals: { products: @products, pagy: @pagy, search_term: @search_term,
|
||||
producer_options: producers, producer_id: @producer_id,
|
||||
category_options: categories, category_id: @category_id,
|
||||
tax_category_options:, flashes: flash,
|
||||
display_search_filter: (@products.any? || @search_term.present? || @category_id.present?) }
|
||||
tax_category_options:, flashes: flash }
|
||||
- %w[product variant].each do |object_type|
|
||||
= render partial: 'delete_modal', locals: { object_type: }
|
||||
#modal-component
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.medium-6#checkout-payment-methods
|
||||
- if @order.distributor.vouchers.present? || @order.distributor.connected_apps.vine.present?
|
||||
- if @order.distributor.vouchers.present?
|
||||
%div.checkout-substep
|
||||
= render partial: "checkout/voucher_section", locals: { order: @order, voucher_adjustment: @order.voucher_adjustments.first }
|
||||
|
||||
|
||||
@@ -81,9 +81,6 @@
|
||||
= t :first_name
|
||||
%th.text-right
|
||||
= t :last_name
|
||||
- if @display_business_name
|
||||
%th.text-right
|
||||
= t :business_name
|
||||
%tbody
|
||||
- @customer_line_items.each do |line_item|
|
||||
%tr
|
||||
@@ -100,9 +97,6 @@
|
||||
= line_item[:first_name]
|
||||
%td
|
||||
= line_item[:last_name]
|
||||
- if @display_business_name
|
||||
%td
|
||||
= line_item[:business_name]
|
||||
%p
|
||||
= t :producer_mail_text_after
|
||||
%p
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#{t :producer_mail_greeting} #{raw(@producer.name)},
|
||||
#{t :producer_mail_greeting} #{@producer.name},
|
||||
\
|
||||
= t :producer_mail_text_before
|
||||
\
|
||||
- @distributors_pickup_times.each do |distributor_name, pickup_time|
|
||||
\- #{raw(distributor_name)} (#{pickup_time})
|
||||
\- #{distributor_name} (#{pickup_time})
|
||||
\
|
||||
- if @receival_instructions
|
||||
= t :producer_mail_delivery_instructions
|
||||
= raw(@receival_instructions)
|
||||
= @receival_instructions
|
||||
\
|
||||
Orders summary
|
||||
================
|
||||
@@ -15,22 +15,22 @@ Orders summary
|
||||
= t :producer_mail_order_text
|
||||
\
|
||||
- @grouped_line_items.each_pair do |product_and_full_name, line_items|
|
||||
#{line_items.first.variant.sku} - #{raw(line_items.first.variant.supplier.name)} - #{raw(product_and_full_name)} (#{t(:producer_mail_qty)}: #{line_items.sum(&:quantity)}) @ #{line_items.first.single_money} = #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency)} (#{t(:with_tax_incl, amount: Spree::Money.new(line_items.sum(&:included_tax), currency: line_items.first.currency))})
|
||||
#{line_items.first.variant.sku} - #{raw(line_items.first.variant.supplier.name)} - #{raw(product_and_full_name)} (QTY: #{line_items.sum(&:quantity)}) @ #{line_items.first.single_money} = #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency)}
|
||||
\
|
||||
\
|
||||
#{t :total}: #{@total} (#{t(:with_tax_incl, amount: @tax_total)})
|
||||
#{t :total}: #{@total}
|
||||
\
|
||||
- if @customer_line_items
|
||||
- if @customer_grouped_line_items
|
||||
= t :producer_mail_order_customer_text
|
||||
\
|
||||
- @customer_line_items.each do |line_item|
|
||||
#{line_item[:sku]} - #{raw(line_item[:supplier_name])} - #{raw(line_item[:product_and_full_name])} (#{t(:producer_mail_qty)}: #{line_item[:quantity]}) - #{raw(line_item[:first_name])} #{raw(line_item[:last_name])} #{raw(line_item[:business_name])}
|
||||
#{line_item[:sku]} - #{raw(line_item[:supplier_name])} - #{raw(line_item[:product_and_full_name])} (QTY: #{line_item[:quantity]}) - #{raw(line_item[:first_name])} #{raw(line_item[:last_name])}
|
||||
\
|
||||
\
|
||||
= t :producer_mail_text_after
|
||||
|
||||
#{t :producer_mail_signoff},
|
||||
#{raw(@coordinator.name)}
|
||||
#{raw(@coordinator.address.address1)}, #{raw(@coordinator.address.city)}, #{raw(@coordinator.address.zipcode)}
|
||||
#{@coordinator.name}
|
||||
#{@coordinator.address.address1}, #{@coordinator.address.city}, #{@coordinator.address.zipcode}
|
||||
#{@coordinator.phone}
|
||||
#{@coordinator.contact.email}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
= f.field_container :tax_category_id do
|
||||
= f.label :tax_category_id, t(:tax_category)
|
||||
%br
|
||||
= f.collection_select(:tax_category_id, Spree::TaxCategory.all, :id, :name, tax_category_dropdown_options(Spree::Config.products_require_tax_category), {:class => "select2 fullwidth"})
|
||||
= f.collection_select(:tax_category_id, Spree::TaxCategory.all, :id, :name, {:include_blank => Spree::Config.products_require_tax_category ? false : t(:none)}, {:class => "select2 fullwidth"})
|
||||
= f.error_message_on :tax_category_id
|
||||
|
||||
@@ -43,10 +43,7 @@
|
||||
- checkout_adjustments_for(@order, exclude: [:line_item]).reverse_each do |adjustment|
|
||||
%tr
|
||||
%td{align: "right", colspan: "3"}
|
||||
- if adjustment.originator_type == "Voucher"
|
||||
= t(:email_order_summary_voucher_label, code: adjustment.label)
|
||||
- else
|
||||
= "#{adjustment.label}:"
|
||||
= "#{adjustment.label}:"
|
||||
%td{align: "right"}
|
||||
= adjustment.display_amount
|
||||
%tr
|
||||
|
||||
@@ -13,7 +13,6 @@ import { ru } from "flatpickr/dist/l10n/ru";
|
||||
import { sv } from "flatpickr/dist/l10n/sv";
|
||||
import { tr } from "flatpickr/dist/l10n/tr";
|
||||
import { en } from "flatpickr/dist/l10n/default.js";
|
||||
import { hu } from "flatpickr/dist/l10n/hu";
|
||||
import ShortcutButtonsPlugin from "shortcut-buttons-flatpickr";
|
||||
import labelPlugin from "flatpickr/dist/plugins/labelPlugin/labelPlugin";
|
||||
|
||||
@@ -37,7 +36,6 @@ export default class extends Flatpickr {
|
||||
sv: sv,
|
||||
tr: tr,
|
||||
en: en,
|
||||
hu: hu,
|
||||
};
|
||||
|
||||
initialize() {
|
||||
|
||||
@@ -12,8 +12,8 @@ export const useOpenAndCloseAsAModal = (controller) => {
|
||||
}.bind(controller),
|
||||
|
||||
close: function (_event, remove = false) {
|
||||
// Only execute close if the current modal is open
|
||||
if (!this.modalTarget.classList.contains('in')) return;
|
||||
// Only execute close if there is an open modal
|
||||
if (!document.querySelector("body").classList.contains('modal-open')) return;
|
||||
|
||||
this.modalTarget.classList.remove("in");
|
||||
this.backgroundTarget.classList.remove("in");
|
||||
|
||||
@@ -10,8 +10,7 @@ export default class extends Controller {
|
||||
|
||||
changePage(event) {
|
||||
const productsForm = document.querySelector("#products-form");
|
||||
if (productsForm) productsForm.scrollIntoView({ behavior: "smooth" });
|
||||
|
||||
productsForm.scrollIntoView({ behavior: "smooth" });
|
||||
this.page.value = event.target.dataset.page;
|
||||
this.submitSearch();
|
||||
this.page.value = 1;
|
||||
|
||||
@@ -64,6 +64,14 @@ products .filter-box {
|
||||
li.active a {
|
||||
color: $clr-brick;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $clr-brick;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: $clr-brick-bright;
|
||||
}
|
||||
@@ -94,10 +102,43 @@ products .filter-box {
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $clr-brick-bright;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $clr-brick-bright;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active, &.active {
|
||||
color: $clr-brick;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $clr-brick;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render-svg {
|
||||
display: block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin: 0 0.2rem 0 0;
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
||||
path {
|
||||
fill: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,32 @@
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
render-svg {
|
||||
&, & svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
float: left;
|
||||
padding-right: 0.25rem;
|
||||
|
||||
path {
|
||||
@include csstrans;
|
||||
|
||||
fill: $base-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: $hover-clr;
|
||||
color: $hover-clr;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $hover-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
@@ -46,6 +69,14 @@
|
||||
&:hover, &:focus {
|
||||
border-color: $border-clr;
|
||||
color: $base-clr;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: $base-clr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +84,14 @@
|
||||
border: 1px solid $base-clr;
|
||||
background: $base-clr;
|
||||
color: white;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
path {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,66 @@
|
||||
|
||||
padding: 0.25rem 0.5rem 0.35rem 0.35rem;
|
||||
|
||||
render-svg {
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
path {
|
||||
fill: #555;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&, & * {
|
||||
display: inline-block;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
.inactive {
|
||||
.fat-taxons {
|
||||
render-svg {}
|
||||
|
||||
svg {
|
||||
path {
|
||||
fill: $disabled-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.product-header {
|
||||
render-svg {
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
path {
|
||||
fill: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(phablet) {
|
||||
render-svg {
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
render-svg {
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
path {
|
||||
fill: $clr-brick;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,24 +8,10 @@ document.addEventListener("turbo:frame-missing", (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// show error message instead
|
||||
showError(event.detail.response?.status);
|
||||
});
|
||||
|
||||
document.addEventListener("turbo:submit-end", (event) => {
|
||||
if (!event.detail.success){
|
||||
// show error message on failure
|
||||
showError(event.detail.fetchResponse?.statusCode);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
function showError(status) {
|
||||
// Note that other 4xx errors will be handled differently.
|
||||
status = event.detail.response.status;
|
||||
if(status == 401) {
|
||||
alert(I18n.t("errors.unauthorized.message"));
|
||||
} else if(status === undefined) {
|
||||
alert(I18n.t("errors.network_error.message"));
|
||||
} else if (status >= 500) {
|
||||
} else {
|
||||
alert(I18n.t("errors.general_error.message"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,9 +6,5 @@ Bugsnag.configure do |config|
|
||||
config.logger = Logger.new(STDOUT)
|
||||
config.logger.level = Logger::ERROR
|
||||
end
|
||||
|
||||
# If you want to notify Bugsnag in dev or test then set the env var:
|
||||
# spring stop
|
||||
# BUGSNAG=true ./bin/rails console
|
||||
config.notify_release_stages = %w(production staging) unless ENV["BUGSNAG"]
|
||||
config.notify_release_stages = %w(production staging)
|
||||
end
|
||||
|
||||
@@ -1138,6 +1138,14 @@ ar:
|
||||
discover_regen:
|
||||
tagline: "By clicking ‘Allow data sharing’, you are sharing data between Open Food Network and the Discover Regenerative Portal, and agreeing to make specific data public on the Portal, which will also be publicly available on the API.\nIMPORTANT: Before you stop sharing and remove your listing, please first contact hello@regenerative.org.au to request a copy of your Discover Regenerative data."
|
||||
loading: "جار التحميل"
|
||||
description_html: |
|
||||
<p>
|
||||
Discover Regenerative is a showcase of Australia’s Regenerative Farmers, their produce/products, outcomes and achievements. It simplifies how business-to-business (B2B) / wholesale buyers can find regenerative produce and directly connect with Producers.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://regenerative.org.au/" target="_blank"><b>Visit Discover Regenerative</b>
|
||||
<i class="icon-external-link"></i></a>
|
||||
</p>
|
||||
vine:
|
||||
enable: "الاتصال"
|
||||
actions:
|
||||
@@ -2968,7 +2976,6 @@ ar:
|
||||
ready: جاهز
|
||||
pending: قيد الانتظار
|
||||
shipped: تم الشحن
|
||||
business_name: الاسم التجاري
|
||||
js:
|
||||
saving: 'حفظ...'
|
||||
changes_saved: 'تم حفظ التغييرات.'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user