Compare commits

...

80 Commits

Author SHA1 Message Date
Luis Ramos
eafffa2c23 Update all locales with the latest Transifex translations 2020-04-10 13:08:11 +01:00
Luis Ramos
82a4753eec Merge pull request #5191 from openfoodfoundation/transifex
Transifex
2020-04-10 13:05:30 +01:00
Matt-Yorkley
b92e858448 Merge pull request #5197 from Matt-Yorkley/cart-populate-performance
Cart populate performance
2020-04-10 11:41:56 +02:00
Luis Ramos
b5ba2acb21 Merge pull request #5169 from jeduardo824/enhancement/5102-make-shop-names-links
make shop name a link on /account
2020-04-09 22:01:34 +01:00
Transifex-Openfoodnetwork
e97a16cb40 Updating translations for config/locales/ca.yml 2020-04-10 04:14:21 +10:00
Matt-Yorkley
57d87a8042 Eager-load variant stock items
Avoids another N+1
2020-04-09 19:55:08 +02:00
Matt-Yorkley
0ca87580e8 Load variants in cart in one query
Avoids an N+1
2020-04-09 19:55:06 +02:00
Luis Ramos
59e0f3d9f4 Merge pull request #5175 from Matt-Yorkley/memoize-scoper
Memoize ScopeProductToHub in product list
2020-04-09 14:45:57 +01:00
Luis Ramos
fc5aff8c79 Merge pull request #5145 from luisramos0/inv_perf
Performance improvements to Inventory page
2020-04-09 13:56:33 +01:00
Luis Ramos
dd717fe8ac Merge pull request #5184 from Matt-Yorkley/inventory-loading
Inventory loading
2020-04-09 13:26:12 +01:00
Luis Ramos
6341c5fd80 Merge pull request #4964 from luisramos0/po_fix
Fix proxy orders controller in rails 4 by removing the use of responders
2020-04-09 10:12:33 +01:00
Matt-Yorkley
47ac6c1491 Remove unused methods from ProductSimpleSerializer 2020-04-09 09:51:32 +02:00
Matt-Yorkley
6afda141a1 Remove track_inventory_levels conditional
This value is always true in OFN
2020-04-09 09:19:37 +02:00
Matt-Yorkley
5bb2614f9d Refactor PagedFetcher so it makes one request at a time 2020-04-09 09:19:37 +02:00
Matt-Yorkley
b3c968856b Fix some rubocop issues 2020-04-09 09:19:37 +02:00
Matt-Yorkley
b0a7497f2a Remove another N+1 in product serialization 2020-04-09 09:19:37 +02:00
Matt-Yorkley
f959e632ea Modify Spree::Stock::Quantifier to not re-fetch stock items if they are already eager-loaded
This helps to remove a big N+1 here, and will also be very helpful elsewhere in the app
2020-04-09 09:19:37 +02:00
Matt-Yorkley
f9cf826f1c Bring Spree::Stock::Quantifier in to OFN
This is the original unmodified Class from Spree. Modifications added in subsequent commits.
2020-04-09 09:19:36 +02:00
Matt-Yorkley
ececbce596 Only select id in producers query 2020-04-09 09:16:44 +02:00
Matt-Yorkley
1b7ac1a252 Don't re-use fat serializers when thin ones are needed.
This cuts the pageload and query count in half, again.
2020-04-09 09:10:41 +02:00
Transifex-Openfoodnetwork
d31b24786a Updating translations for config/locales/en_NZ.yml 2020-04-09 16:16:24 +10:00
Matt-Yorkley
374bf04118 Merge pull request #5142 from Matt-Yorkley/shops-firefighting
Don't load distributed properties on inactive distributors
2020-04-08 20:45:01 +02:00
Matt-Yorkley
3aff7f62e3 Don't query distributed properties on enterprises that aren't active distributors
Cuts page load on /shops by ~75% (with production data) and removes ~300 expensive and superfluous queries.
2020-04-08 20:08:12 +02:00
Matt-Yorkley
fc5e346a06 Merge pull request #5156 from Matt-Yorkley/closed-shops
Load closed shops in a separate request on /shops page
2020-04-08 20:05:26 +02:00
Luis Ramos
29bbf2fa74 Merge pull request #5088 from coopdevs/do-not-recreate-when-booting-docker
Do not reset the dev env when booting docker
2020-04-07 21:56:09 +01:00
Matt-Yorkley
64c66ddedc Eager-load variant data for overridable products
Cuts query count and page load time in half for this endpoint.
2020-04-07 15:16:32 +02:00
Luis Ramos
e0e2c32d9f Merge pull request #5177 from openfoodfoundation/dependabot/bundler/oj-3.10.6
Bump oj from 3.10.5 to 3.10.6
2020-04-07 11:44:09 +01:00
Matt-Yorkley
003341ef7a Add loading indicator when showing closed shops 2020-04-07 10:40:49 +02:00
Matt-Yorkley
94f8ea2f93 Fix flicker effect showing 3 buttons when clicking "Show Closed Shops" button 2020-04-07 10:31:56 +02:00
Maikel
eb64112b22 Merge pull request #5144 from openfoodfoundation/transifex
Transifex
2020-04-07 16:23:35 +10:00
Maikel
3e14b62b46 Merge pull request #5136 from openfoodfoundation/dependabot/bundler/ddtrace-0.34.1
Bump ddtrace from 0.34.0 to 0.34.1
2020-04-07 16:05:53 +10:00
Maikel
3244650932 Merge pull request #5135 from openfoodfoundation/dependabot/bundler/rubocop-rails-2.5.1
Bump rubocop-rails from 2.5.0 to 2.5.1
2020-04-07 16:05:18 +10:00
Transifex-Openfoodnetwork
b6753a2593 Updating translations for config/locales/fil_PH.yml 2020-04-07 16:03:57 +10:00
Transifex-Openfoodnetwork
1b119805b4 Updating translations for config/locales/fil_PH.yml 2020-04-07 15:15:12 +10:00
Transifex-Openfoodnetwork
edde7689a9 Updating translations for config/locales/de_DE.yml 2020-04-07 13:57:54 +10:00
Transifex-Openfoodnetwork
8060977786 Updating translations for config/locales/en_GB.yml 2020-04-07 13:54:53 +10:00
Transifex-Openfoodnetwork
837a345958 Updating translations for config/locales/en_FR.yml 2020-04-07 13:54:46 +10:00
dependabot-preview[bot]
7c5e511fde Bump oj from 3.10.5 to 3.10.6
Bumps [oj](https://github.com/ohler55/oj) from 3.10.5 to 3.10.6.
- [Release notes](https://github.com/ohler55/oj/releases)
- [Changelog](https://github.com/ohler55/oj/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/ohler55/oj/compare/v3.10.5...v3.10.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-04-06 19:24:48 +00:00
Luis Ramos
d18a06a0f7 Merge pull request #4783 from luisramos0/stripe_sca_extra_subs
Move all subscriptions services to the OrderManagement engine
2020-04-06 20:20:02 +01:00
Luis Ramos
d23b4fd307 Merge pull request #5174 from coopdevs/change-pool-size
Allow changing the connection pool size
2020-04-06 19:29:09 +01:00
Matt-Yorkley
2cb4c6bec2 Memoize OpenFoodNetwork::ScopeProductToHub
This means we avoid fetching all of the hub's variants every time we scope a product. Applies to every product loaded when displaying a shops's product list.
2020-04-06 19:41:05 +02:00
Pau Pérez Fabregat
924e816a5b Merge pull request #5151 from luisramos0/fix_order_perms
Fix Permissions::Order spec in rails 4
2020-04-06 18:07:39 +02:00
Pau Pérez Fabregat
109da43905 Merge pull request #2 from luisramos0/do-not-recreate-when-booting-docker
Re-add setup instructions removed from docker-compose into Dockerfile…
2020-04-06 16:55:59 +02:00
Pau Perez
33ca6a2096 Allow changing the connection pool size
This allows us to tune for UK. The hypothesis from @kristinalim is:

> From what I understand, it can result to Rails processes waiting for
each other to complete, while the DB server can take more simultaneous
connections.
2020-04-06 16:03:06 +02:00
Eduardo
e7b780f963 make shop name a link on /account 2020-04-06 08:34:24 -03:00
dependabot-preview[bot]
13cba3d244 Bump ddtrace from 0.34.0 to 0.34.1
Bumps [ddtrace](https://github.com/DataDog/dd-trace-rb) from 0.34.0 to 0.34.1.
- [Release notes](https://github.com/DataDog/dd-trace-rb/releases)
- [Changelog](https://github.com/DataDog/dd-trace-rb/blob/master/CHANGELOG.md)
- [Commits](https://github.com/DataDog/dd-trace-rb/compare/v0.34.0...v0.34.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-04-06 11:27:10 +00:00
Matt-Yorkley
ce45e7cf71 Merge pull request #5162 from Matt-Yorkley/unicorn-worker-killer
Add optional unicorn-worker-killer configs
2020-04-06 13:25:22 +02:00
Matt-Yorkley
ba5a56db14 Make upper and lower bounds configurable 2020-04-06 10:39:51 +02:00
Matt-Yorkley
276dcf4a3b Add optional unicorn-worker-killer configs 2020-04-06 10:39:50 +02:00
Transifex-Openfoodnetwork
dcfb1aec6d Updating translations for config/locales/en_PH.yml 2020-04-06 18:11:07 +10:00
Pau Pérez Fabregat
fde2aac366 Merge pull request #5163 from coopdevs/collect-Ruby-GC
Enable Ruby Runtime Metrics in Datadog
2020-04-05 14:32:54 +02:00
Luis Ramos
63138aef30 Re-add setup instructions removed from docker-compose into Dockerfile and Docker.md 2020-04-05 00:06:31 +01:00
Pau Perez
7612415991 Enable Ruby Runtime Metrics in Datadog
This automatically collects a bunch of Ruby's GC-related metrics that
will come in handy while we tune the Unicorn workers. Some of theres
are:

* runtime.ruby.class_count
* runtime.ruby.gc.malloc_increase_bytes
* runtime.ruby.gc.total_allocated_objects
* runtime.ruby.gc.total_freed_objects
* runtime.ruby.gc.heap_marked_slots
* runtime.ruby.gc.heap_available_slots
* runtime.ruby.gc.heap_free_slots
* runtime.ruby.thread_count

Check https://docs.datadoghq.com/tracing/runtime_metrics/ruby/#data-collected for the complete list.

The cool thing is that

> Runtime metrics can be viewed in correlation with your Ruby services
2020-04-05 00:16:10 +02:00
Luis Ramos
53d901b41b Merge pull request #5064 from luisramos0/fix_sample_data
Fix sample data and custom paper_trail config on order_cycles and schedules on rails 4
2020-04-04 18:29:26 +01:00
Matt-Yorkley
bc859cf9f7 Add api/shops_controller and refactor 2020-04-04 17:02:27 +02:00
Matt-Yorkley
af48cac140 Load closed shops in a separate request on /shops page 2020-04-04 14:06:10 +02:00
Luis Ramos
59bb956677 Merge pull request #5010 from Matt-Yorkley/remove_simple_form
Remove simple_form
2020-04-04 11:53:06 +01:00
Luis Ramos
55b3f4d54f Move search params test case to a different context so that we dont have to set the producer of the products in the order
This is working in master by chance of the factories but breaks in rails 4 because the orders in this test dont have products supplied by the producer which is a necessary condition in the context where it was
2020-04-03 19:47:33 +01:00
Luis Ramos
452ab3a842 Add comment to better explain variant_override_set.collection_to_delete 2020-04-03 15:36:19 +01:00
Luis Ramos
a049e7a433 Add product to includes to avoid N+1 queries to fetch products when VO authorization is done right after this 2020-04-03 14:48:06 +01:00
Transifex-Openfoodnetwork
97063bf47e Updating translations for config/locales/en_GB.yml 2020-04-03 23:24:25 +11:00
dependabot-preview[bot]
5b4dd57380 Bump rubocop-rails from 2.5.0 to 2.5.1
Bumps [rubocop-rails](https://github.com/rubocop-hq/rubocop-rails) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/rubocop-hq/rubocop-rails/releases)
- [Changelog](https://github.com/rubocop-hq/rubocop-rails/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop-hq/rubocop-rails/compare/v2.5.0...v2.5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-04-02 19:15:11 +00:00
Pau Perez
904e89e325 Do not reset the dev env when booting docker
The current web container's command destroys anything you might have in
your local DB from a previous session, assuming you always want start
from a clean environment. This is hardly the case and makes
`docker-compose up` take quite long. What if you just stopped containers
temporally while developing?

This changes the approach to not assume anything. If you need to install
a new gem or reset your DB just run the commands you would without
docker. You can run anything you want with `docker-compose run web bundle exec
<rails/rake command>` anyway.

For someone setting things for the first time, the `Dockerfile` process
still installs all dependencies.
2020-03-27 12:17:09 +01:00
Luis Ramos
e5e9325499 Fix paper_trail custom_data for order_cycle, custom data must be a string, cant be an array 2020-03-25 15:35:51 +00:00
Luis Ramos
2e4f8003b6 Fix group factory in rails 4
params[:address] was breaking the creation of the EnterpriseGroup
2020-03-25 15:32:16 +00:00
Matt-Yorkley
45b5e838b7 Remove simple_form
It looks like we don't use this at all. Discovered during the Spree 2.1 upgrade.
2020-03-17 17:03:48 +01:00
Luis Ramos
f8a4f00d52 Fix rubocop issues in subs specs 2020-03-16 17:20:01 +00:00
Luis Ramos
29377bbff9 Move 5 subscriptions services from app/services to the engines/order_management/app/services 2020-03-16 17:20:01 +00:00
Luis Ramos
f68d0c2a0f Remove Subscription from the name of the subscription summarizer and summary because it is already in the namespace 2020-03-16 17:20:01 +00:00
Luis Ramos
3901c49af9 Fix rubocop issues 2020-03-16 17:20:01 +00:00
Luis Ramos
ae0ceb61a1 Move ProxyOrderSyncer to OrderManagement engine 2020-03-16 17:20:01 +00:00
Luis Ramos
fb1c825fbc Move both subscription summarizer and subscription summary to order management engine 2020-03-16 17:20:01 +00:00
Luis Ramos
e36b0249b9 Use nested module names to fix rubocpo issue 2020-03-16 17:20:01 +00:00
Luis Ramos
34fa2d7ad6 Move Subscriptions::PaymentSetup to OrderManagement engine where all subscription code will be at some point in the future 2020-03-16 17:19:04 +00:00
Luis Ramos
3aefea9f04 Prepare SubsConfirmJob to receive a bit more payment logic 2020-03-16 17:19:04 +00:00
Luis Ramos
15231a9128 Make SubsConfirmJob more readable 2020-03-16 17:19:04 +00:00
Luis Ramos
25e3f72934 Fix rubocop issues in subs payment_setup 2020-03-16 17:19:04 +00:00
Luis Ramos
523d819575 Move and rename SubscriptionPaymentUpdater to Subscriptios::PaymentSetup to move to services/subscriptions and call it Setup instead to make explicit this is executed before the payment is processed 2020-03-16 17:16:31 +00:00
Luis Ramos
bc0a1d9bae Remove one more responder and fix rubocop issues 2020-03-10 15:56:08 +00:00
Luis Ramos
a53dc3a8c1 Remove usage of the responder as this is a json only controller 2020-03-10 14:46:16 +00:00
127 changed files with 9911 additions and 2814 deletions

View File

@@ -33,7 +33,6 @@ Layout/LineLength:
- app/controllers/admin/inventory_items_controller.rb
- app/controllers/admin/manager_invitations_controller.rb
- app/controllers/admin/product_import_controller.rb
- app/controllers/admin/proxy_orders_controller.rb
- app/controllers/admin/schedules_controller.rb
- app/controllers/admin/subscriptions_controller.rb
- app/controllers/admin/variant_overrides_controller.rb
@@ -98,7 +97,6 @@ Layout/LineLength:
- app/services/embedded_page_service.rb
- app/services/order_cycle_form.rb
- app/services/order_factory.rb
- app/services/subscriptions_count.rb
- app/services/variants_stock_levels.rb
- engines/web/app/helpers/web/cookies_policy_helper.rb
- lib/discourse/single_sign_on.rb
@@ -239,10 +237,7 @@ Layout/LineLength:
- spec/lib/open_food_network/packing_report_spec.rb
- spec/lib/open_food_network/permissions_spec.rb
- spec/lib/open_food_network/products_and_inventory_report_spec.rb
- spec/lib/open_food_network/proxy_order_syncer_spec.rb
- spec/lib/open_food_network/scope_variant_to_hub_spec.rb
- spec/lib/open_food_network/subscription_payment_updater_spec.rb
- spec/lib/open_food_network/subscription_summarizer_spec.rb
- spec/lib/open_food_network/tag_rule_applicator_spec.rb
- spec/lib/open_food_network/users_and_enterprises_report_spec.rb
- spec/lib/open_food_network/xero_invoices_report_spec.rb
@@ -317,10 +312,6 @@ Layout/LineLength:
- spec/services/permissions/order_spec.rb
- spec/services/product_tag_rules_filterer_spec.rb
- spec/services/products_renderer_spec.rb
- spec/services/subscription_estimator_spec.rb
- spec/services/subscription_form_spec.rb
- spec/services/subscription_validator_spec.rb
- spec/services/subscription_variants_service_spec.rb
- spec/spec_helper.rb
- spec/support/cancan_helper.rb
- spec/support/delayed_job_helper.rb
@@ -411,7 +402,7 @@ Metrics/AbcSize:
- app/services/cart_service.rb
- app/services/create_order_cycle.rb
- app/services/order_syncer.rb
- app/services/subscription_validator.rb
- engines/order_management/app/services/order_management/subscriptions/validator.rb
- lib/active_merchant/billing/gateways/stripe_decorator.rb
- lib/active_merchant/billing/gateways/stripe_payment_intents.rb
- lib/discourse/single_sign_on.rb
@@ -686,6 +677,13 @@ Metrics/ModuleLength:
- app/helpers/injection_helper.rb
- app/helpers/spree/admin/navigation_helper.rb
- app/helpers/spree/admin/base_helper.rb
- engines/order_management/spec/services/order_management/subscriptions/estimator_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/form_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/proxy_order_syncer_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/payment_setup_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/summarizer_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/validator_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/variants_list_spec.rb
- lib/open_food_network/column_preference_defaults.rb
- spec/controllers/admin/enterprises_controller_spec.rb
- spec/controllers/admin/order_cycles_controller_spec.rb
@@ -701,9 +699,7 @@ Metrics/ModuleLength:
- spec/lib/open_food_network/order_grouper_spec.rb
- spec/lib/open_food_network/permissions_spec.rb
- spec/lib/open_food_network/products_and_inventory_report_spec.rb
- spec/lib/open_food_network/proxy_order_syncer_spec.rb
- spec/lib/open_food_network/scope_variant_to_hub_spec.rb
- spec/lib/open_food_network/subscription_payment_updater_spec.rb
- spec/lib/open_food_network/tag_rule_applicator_spec.rb
- spec/lib/open_food_network/users_and_enterprises_report_spec.rb
- spec/models/spree/ability_spec.rb

View File

@@ -71,7 +71,6 @@ Lint/DuplicateHashKey:
Lint/DuplicateMethods:
Exclude:
- 'lib/discourse/single_sign_on.rb'
- 'lib/open_food_network/subscription_summary.rb'
# Offense count: 10
Lint/IneffectiveAccessModifier:
@@ -159,7 +158,7 @@ Naming/MethodParameterName:
Exclude:
- 'app/helpers/spree/admin/base_helper_decorator.rb'
- 'app/helpers/spree/base_helper_decorator.rb'
- 'app/services/subscription_validator.rb'
- 'engines/order_management/app/services/order_management/subscriptions/validator.rb'
- 'lib/open_food_network/reports/bulk_coop_report.rb'
- 'lib/open_food_network/xero_invoices_report.rb'
- 'spec/lib/open_food_network/reports/report_spec.rb'
@@ -849,11 +848,6 @@ Style/FrozenStringLiteralComment:
- 'app/services/reset_order_service.rb'
- 'app/services/restart_checkout.rb'
- 'app/services/search_orders.rb'
- 'app/services/subscription_estimator.rb'
- 'app/services/subscription_form.rb'
- 'app/services/subscription_validator.rb'
- 'app/services/subscription_variants_service.rb'
- 'app/services/subscriptions_count.rb'
- 'app/services/tax_rate_finder.rb'
- 'app/services/upload_sanitizer.rb'
- 'app/services/variant_deleter.rb'
@@ -892,6 +886,7 @@ Style/FrozenStringLiteralComment:
- 'engines/order_management/lib/order_management/engine.rb'
- 'engines/order_management/lib/order_management/version.rb'
- 'engines/order_management/order_management.gemspec'
- 'engines/order_management/spec/performance/order_management/subscriptions/proxy_order_syncer_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/authorizer_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/parameters_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/permissions_spec.rb'
@@ -949,7 +944,6 @@ Style/FrozenStringLiteralComment:
- 'lib/open_food_network/products_and_inventory_report.rb'
- 'lib/open_food_network/products_and_inventory_report_base.rb'
- 'lib/open_food_network/property_merge.rb'
- 'lib/open_food_network/proxy_order_syncer.rb'
- 'lib/open_food_network/rack_request_blocker.rb'
- 'lib/open_food_network/referer_parser.rb'
- 'lib/open_food_network/reports/bulk_coop_allocation_report.rb'
@@ -966,8 +960,6 @@ Style/FrozenStringLiteralComment:
- 'lib/open_food_network/scope_variants_for_search.rb'
- 'lib/open_food_network/spree_api_key_loader.rb'
- 'lib/open_food_network/subscription_payment_updater.rb'
- 'lib/open_food_network/subscription_summarizer.rb'
- 'lib/open_food_network/subscription_summary.rb'
- 'lib/open_food_network/tag_rule_applicator.rb'
- 'lib/open_food_network/user_balance_calculator.rb'
- 'lib/open_food_network/users_and_enterprises_report.rb'
@@ -1209,7 +1201,6 @@ Style/FrozenStringLiteralComment:
- 'spec/lib/open_food_network/permissions_spec.rb'
- 'spec/lib/open_food_network/products_and_inventory_report_spec.rb'
- 'spec/lib/open_food_network/property_merge_spec.rb'
- 'spec/lib/open_food_network/proxy_order_syncer_spec.rb'
- 'spec/lib/open_food_network/referer_parser_spec.rb'
- 'spec/lib/open_food_network/reports/report_spec.rb'
- 'spec/lib/open_food_network/reports/row_spec.rb'
@@ -1218,8 +1209,6 @@ Style/FrozenStringLiteralComment:
- 'spec/lib/open_food_network/scope_variant_to_hub_spec.rb'
- 'spec/lib/open_food_network/scope_variants_to_search_spec.rb'
- 'spec/lib/open_food_network/subscription_payment_updater_spec.rb'
- 'spec/lib/open_food_network/subscription_summarizer_spec.rb'
- 'spec/lib/open_food_network/subscription_summary_spec.rb'
- 'spec/lib/open_food_network/tag_rule_applicator_spec.rb'
- 'spec/lib/open_food_network/user_balance_calculator_spec.rb'
- 'spec/lib/open_food_network/users_and_enterprises_report_spec.rb'
@@ -1304,7 +1293,6 @@ Style/FrozenStringLiteralComment:
- 'spec/models/variant_override_spec.rb'
- 'spec/performance/injection_helper_spec.rb'
- 'spec/performance/orders_controller_spec.rb'
- 'spec/performance/proxy_order_syncer_spec.rb'
- 'spec/performance/shop_controller_spec.rb'
- 'spec/requests/checkout/failed_checkout_spec.rb'
- 'spec/requests/checkout/paypal_spec.rb'
@@ -1355,11 +1343,6 @@ Style/FrozenStringLiteralComment:
- 'spec/services/reset_order_service_spec.rb'
- 'spec/services/restart_checkout_spec.rb'
- 'spec/services/search_orders_spec.rb'
- 'spec/services/subscription_estimator_spec.rb'
- 'spec/services/subscription_form_spec.rb'
- 'spec/services/subscription_validator_spec.rb'
- 'spec/services/subscription_variants_service_spec.rb'
- 'spec/services/subscriptions_count_spec.rb'
- 'spec/services/tax_rate_finder_spec.rb'
- 'spec/services/upload_sanitizer_spec.rb'
- 'spec/services/variants_stock_levels_spec.rb'
@@ -1557,7 +1540,6 @@ Style/Send:
- 'spec/lib/open_food_network/products_and_inventory_report_spec.rb'
- 'spec/lib/open_food_network/sales_tax_report_spec.rb'
- 'spec/lib/open_food_network/subscription_payment_updater_spec.rb'
- 'spec/lib/open_food_network/subscription_summarizer_spec.rb'
- 'spec/lib/open_food_network/tag_rule_applicator_spec.rb'
- 'spec/lib/open_food_network/xero_invoices_report_spec.rb'
- 'spec/lib/stripe/webhook_handler_spec.rb'

View File

@@ -35,13 +35,20 @@ Download the Docker images and build the containers:
$ docker-compose build
```
Run the app with all the required containers:
Setup the database and seed it with sample data:
```sh
$ docker-compose run web bundle exec rake db:reset
$ docker-compose run web bundle exec rake db:test:prepare
$ docker-compose run web bundle exec rake ofn:sample_data
```
Finally, run the app with all the required containers:
```sh
$ docker-compose up
```
This command will setup the database and seed it with sample data. The default admin user is 'ofn@example.com' with 'ofn123' password.
The default admin user is 'ofn@example.com' with 'ofn123' password.
Check the app in the browser at `http://localhost:3000`.
You will then get the trace of the containers in the terminal. You can stop the containers using Ctrl-C in the terminal.

View File

@@ -12,7 +12,7 @@ ENV BUNDLE_PATH /bundles
WORKDIR /usr/src/app
COPY .ruby-version .
# Rbenv & Ruby part
# Install Rbenv & Ruby
RUN git clone https://github.com/rbenv/rbenv.git ${RBENV_ROOT} && \
git clone https://github.com/rbenv/ruby-build.git ${RBENV_ROOT}/plugins/ruby-build && \
${RBENV_ROOT}/plugins/ruby-build/install.sh && \
@@ -21,7 +21,7 @@ RUN git clone https://github.com/rbenv/rbenv.git ${RBENV_ROOT} && \
rbenv global $(cat .ruby-version) && \
gem install bundler --version=1.17.2
# Postgres
# Install Postgres
RUN sh -c "echo 'deb https://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main' > /etc/apt/sources.list.d/pgdg.list" && \
wget --quiet -O - https://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | apt-key add - && \
apt-get update && \
@@ -38,4 +38,6 @@ RUN wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.z
unzip chromedriver_linux64.zip -d /usr/bin && \
chmod u+x /usr/bin/chromedriver
# Copy code and install app dependencies
COPY . /usr/src/app/
RUN bundle install

View File

@@ -45,10 +45,6 @@ gem 'daemons'
gem 'delayed_job_active_record'
gem 'delayed_job_web'
# Fix bug in simple_form preventing collection_check_boxes usage within form_for block
# When merged, revert to upstream gem
gem 'simple_form', github: 'RohanM/simple_form'
# Spree's default pagination gem (locked to the current version used by Spree)
# We use it's methods in OFN code as well, so this is a direct dependency
gem 'kaminari', '~> 0.14.1'
@@ -122,6 +118,7 @@ gem 'ofn-qz', github: 'openfoodfoundation/ofn-qz', ref: '60da2ae4c44cbb4c8d602f5
group :production, :staging do
gem 'ddtrace'
gem 'unicorn-worker-killer'
end
group :test, :development do

View File

@@ -1,11 +1,3 @@
GIT
remote: https://github.com/RohanM/simple_form.git
revision: 45f08a213b40f3d4bda5f5398db841137587160a
specs:
simple_form (2.0.2)
actionpack (~> 3.0)
activemodel (~> 3.0)
GIT
remote: https://github.com/jeremydurham/custom-err-msg.git
revision: 3a8ec9dddc7a5b0aab7c69a6060596de300c68f4
@@ -206,7 +198,7 @@ GEM
activerecord (>= 3.2.0, < 5.0)
fog (~> 1.0)
rails (>= 3.2.0, < 5.0)
ddtrace (0.34.0)
ddtrace (0.34.1)
msgpack
debugger-linecache (1.2.0)
deface (1.0.2)
@@ -411,6 +403,8 @@ GEM
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
geocoder (1.1.8)
get_process_mem (0.2.5)
ffi (~> 1.0)
gmaps4rails (1.5.6)
haml (4.0.7)
tilt
@@ -475,7 +469,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.10.5)
oj (3.10.6)
orm_adapter (0.5.0)
paper_trail (5.2.3)
activerecord (>= 3.0, < 6.0)
@@ -602,7 +596,7 @@ GEM
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-rails (2.5.0)
rubocop-rails (2.5.1)
activesupport
rack (>= 1.1)
rubocop (>= 0.72.0)
@@ -668,6 +662,9 @@ GEM
unicorn-rails (2.2.1)
rack
unicorn
unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uuidtools (2.1.5)
warden (1.2.7)
rack (>= 1.0)
@@ -776,7 +773,6 @@ DEPENDENCIES
select2-rails (~> 3.4.7)
selenium-webdriver
shoulda-matchers
simple_form!
simplecov
spinjs-rails
spree_core!
@@ -792,6 +788,7 @@ DEPENDENCIES
uglifier (>= 1.0.3)
unicorn
unicorn-rails
unicorn-worker-killer
web!
webdrivers
webmock

View File

@@ -2,19 +2,24 @@ angular.module("admin.indexUtils").factory "PagedFetcher", (dataFetcher) ->
new class PagedFetcher
# Given a URL like http://example.com/foo?page=::page::&per_page=20
# And the response includes an attribute pages with the number of pages to fetch
# Fetch each page async, and call the processData callback with the resulting data
fetch: (url, processData, onLastPageComplete) ->
dataFetcher(@urlForPage(url, 1)).then (data) =>
processData data
# Fetch each page async, and call the pageCallback callback with the resulting data
# Developer note: this class should not be re-used!
page: 1
last_page: 1
if data.pages > 1
for page in [2..data.pages]
lastPromise = dataFetcher(@urlForPage(url, page)).then (data) ->
processData data
onLastPageComplete && lastPromise.then onLastPageComplete
return
else
onLastPageComplete && onLastPageComplete()
fetch: (url, pageCallback) ->
@fetchPages(url, @page, pageCallback)
urlForPage: (url, page) ->
url.replace("::page::", page)
fetchPages: (url, page, pageCallback) ->
dataFetcher(@urlForPage(url, page)).then (data) =>
@page++
@last_page = data.pages
pageCallback(data) if pageCallback
if @page <= @last_page
@fetchPages(url, @page, pageCallback)

View File

@@ -43,12 +43,11 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl",
$scope.fetchProducts = ->
url = "/api/products/overridable?page=::page::;per_page=100"
PagedFetcher.fetch url, (data) => $scope.addProducts data.products
PagedFetcher.fetch url, $scope.addProducts
$scope.addProducts = (products) ->
$scope.products = $scope.products.concat products
VariantOverrides.ensureDataFor hubs, products
$scope.addProducts = (data) ->
$scope.products = $scope.products.concat data.products
VariantOverrides.ensureDataFor hubs, data.products
$scope.displayDirty = ->
if DirtyVariantOverrides.count() > 0

View File

@@ -9,8 +9,13 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location
$scope.show_closed = false
$scope.filtersActive = false
$scope.distanceMatchesShown = false
$scope.closed_shops_loading = false
$scope.closed_shops_loaded = false
$scope.$watch "query", (query)->
$scope.resetSearch(query)
$scope.resetSearch = (query) ->
Enterprises.flagMatching query
Search.search query
$rootScope.$broadcast 'enterprisesChanged'
@@ -19,6 +24,7 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location
$timeout ->
Enterprises.calculateDistance query, $scope.firstNameMatch()
$rootScope.$broadcast 'enterprisesChanged'
$scope.closed_shops_loading = false
$timeout ->
if $location.search()['show_closed']?
@@ -73,6 +79,12 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location
undefined
$scope.showClosedShops = ->
unless $scope.closed_shops_loaded
$scope.closed_shops_loading = true
$scope.closed_shops_loaded = true
Enterprises.loadClosedEnterprises().then ->
$scope.resetSearch($scope.query)
$scope.show_closed = true
$location.search('show_closed', '1')

View File

@@ -24,7 +24,7 @@ Darkswarm.controller "HubNodeCtrl", ($scope, HashNavigation, CurrentHub, $http,
$scope.shopfront_loading = true
$scope.toggle_tab(event)
$http.get("/api/enterprises/" + $scope.hub.id + "/shopfront")
$http.get("/api/shops/" + $scope.hub.id)
.success (data) ->
$scope.shopfront_loading = false
$scope.hub = data

View File

@@ -24,7 +24,7 @@ Darkswarm.controller "ProducerNodeCtrl", ($scope, HashNavigation, $anchorScroll,
$scope.shopfront_loading = true
$scope.toggle_tab(event)
$http.get("/api/enterprises/" + $scope.producer.id + "/shopfront")
$http.get("/api/shops/" + $scope.producer.id)
.success (data) ->
$scope.shopfront_loading = false
$scope.producer = data

View File

@@ -5,7 +5,7 @@ Darkswarm.factory "EnterpriseModal", ($modal, $rootScope, $http)->
scope = $rootScope.$new(true) # Spawn an isolate to contain the enterprise
scope.embedded_layout = window.location.search.indexOf("embedded_shopfront=true") != -1
$http.get("/api/enterprises/" + enterprise.id + "/shopfront").success (data) ->
$http.get("/api/shops/" + enterprise.id).success (data) ->
scope.enterprise = data
$modal.open(templateUrl: "enterprise_modal.html", scope: scope)
.error (data) ->

View File

@@ -1,27 +1,30 @@
Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer, Matcher, Geo, $rootScope) ->
Darkswarm.factory 'Enterprises', (enterprises, ShopsResource, CurrentHub, Taxons, Dereferencer, Matcher, Geo, $rootScope) ->
new class Enterprises
enterprises: []
enterprises_by_id: {}
constructor: ->
# Populate Enterprises.enterprises from json in page.
@enterprises = enterprises
@initEnterprises(enterprises)
initEnterprises: (enterprises) ->
# Map enterprises to id/object pairs for lookup.
for enterprise in enterprises
@enterprises.push enterprise
@enterprises_by_id[enterprise.id] = enterprise
# Replace enterprise and taxons ids with actual objects.
@dereferenceEnterprises()
@dereferenceEnterprises(enterprises)
@producers = @enterprises.filter (enterprise)->
enterprise.category in ["producer_hub", "producer_shop", "producer"]
@hubs = @enterprises.filter (enterprise)->
enterprise.category in ["hub", "hub_profile", "producer_hub", "producer_shop"]
dereferenceEnterprises: ->
dereferenceEnterprises: (enteprises) ->
if CurrentHub.hub?.id
CurrentHub.hub = @enterprises_by_id[CurrentHub.hub.id]
for enterprise in @enterprises
for enterprise in enterprises
@dereferenceEnterprise enterprise
dereferenceEnterprise: (enterprise) ->
@@ -42,6 +45,12 @@ Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer,
for enterprise in new_enterprises
@enterprises_by_id[enterprise.id] = enterprise
loadClosedEnterprises: ->
request = ShopsResource.closed_shops {}, (data) =>
@initEnterprises(data)
request.$promise
flagMatching: (query) ->
for enterprise in @enterprises
enterprise.matches_name_query = if query? && query.length > 0

View File

@@ -0,0 +1,7 @@
Darkswarm.factory 'ShopsResource', ($resource) ->
$resource('/api/shops/:id.json', {}, {
'closed_shops':
method: 'GET'
isArray: true
url: '/api/shops/closed_shops.json'
})

View File

@@ -14,5 +14,10 @@
.more-controls {
text-align: center;
.spinner {
height: 2.25em;
margin-right: 0.5em;
}
}
}

View File

@@ -16,7 +16,7 @@ module Admin
render_as_json @collection,
ams_prefix: params[:ams_prefix],
current_user: spree_current_user,
subscriptions_count: SubscriptionsCount.new(@collection)
subscriptions_count: OrderManagement::Subscriptions::Count.new(@collection)
end
end
end
@@ -74,7 +74,7 @@ module Admin
render_as_json @order_cycles,
ams_prefix: 'index',
current_user: spree_current_user,
subscriptions_count: SubscriptionsCount.new(@collection)
subscriptions_count: OrderManagement::Subscriptions::Count.new(@collection)
else
order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? }
render json: { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity

View File

@@ -9,25 +9,19 @@ module Admin
def cancel
if @proxy_order.cancel
respond_with(@proxy_order) do |format|
format.json { render_as_json @proxy_order }
end
render_as_json @proxy_order
else
respond_with(@proxy_order) do |format|
format.json { render json: { errors: [t('admin.proxy_orders.cancel.could_not_cancel_the_order')] }, status: :unprocessable_entity }
end
render json: { errors: [t('admin.proxy_orders.cancel.could_not_cancel_the_order')] },
status: :unprocessable_entity
end
end
def resume
if @proxy_order.resume
respond_with(@proxy_order) do |format|
format.json { render_as_json @proxy_order }
end
render_as_json @proxy_order
else
respond_with(@proxy_order) do |format|
format.json { render json: { errors: [t('admin.proxy_orders.resume.could_not_resume_the_order')] }, status: :unprocessable_entity }
end
render json: { errors: [t('admin.proxy_orders.resume.could_not_resume_the_order')] },
status: :unprocessable_entity
end
end
end

View File

@@ -1,5 +1,5 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
require 'order_management/subscriptions/proxy_order_syncer'
module Admin
class SchedulesController < ResourceController
@@ -81,7 +81,7 @@ module Admin
return unless removed_ids.any? || new_ids.any?
subscriptions = Subscription.where(schedule_id: @schedule)
syncer = OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions)
syncer = OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions)
syncer.sync!
end
end

View File

@@ -56,7 +56,7 @@ module Admin
end
def variant_if_eligible(variant_id)
SubscriptionVariantsService.eligible_variants(@shop).find_by_id(variant_id)
OrderManagement::Subscriptions::VariantsList.eligible_variants(@shop).find_by_id(variant_id)
end
end
end

View File

@@ -1,5 +1,4 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
module Admin
class SubscriptionsController < ResourceController
@@ -65,7 +64,7 @@ module Admin
private
def save_form_and_render(render_issues = true)
form = SubscriptionForm.new(@subscription, params[:subscription])
form = OrderManagement::Subscriptions::Form.new(@subscription, params[:subscription])
unless form.save
render json: { errors: form.json_errors }, status: :unprocessable_entity
return

View File

@@ -73,8 +73,10 @@ module Admin
end
def collection
@variant_overrides = VariantOverride.includes(:variant).for_hubs(params[:hub_id] || @hubs)
@variant_overrides.select { |vo| vo.variant.present? }
@variant_overrides = VariantOverride.
includes(variant: :product).
for_hubs(params[:hub_id] || @hubs).
select { |vo| vo.variant.present? }
end
def collection_actions

View File

@@ -5,7 +5,6 @@ module Api
before_filter :override_sells, only: [:create, :update]
before_filter :override_visible, only: [:create, :update]
respond_to :json
skip_authorization_check only: [:shopfront]
def create
authorize! :create, Enterprise
@@ -42,12 +41,6 @@ module Api
end
end
def shopfront
enterprise = Enterprise.find_by_id(params[:id])
render text: Api::EnterpriseShopfrontSerializer.new(enterprise).to_json, status: :ok
end
private
def override_owner

View File

@@ -69,12 +69,12 @@ module Api
end
def overridable
producers = OpenFoodNetwork::Permissions.new(current_api_user).
variant_override_producers.by_name
producer_ids = OpenFoodNetwork::Permissions.new(current_api_user).
variant_override_producers.by_name.select('enterprises.id')
@products = paged_products_for_producers producers
@products = paged_products_for_producers producer_ids
render_paged_products @products
render_paged_products @products, ::Api::Admin::ProductSimpleSerializer
end
# POST /api/products/:product_id/clone
@@ -118,19 +118,20 @@ module Api
]
end
def paged_products_for_producers(producers)
def paged_products_for_producers(producer_ids)
Spree::Product.scoped.
merge(product_scope).
where(supplier_id: producers).
includes(variants: [:product, :default_price, :stock_items]).
where(supplier_id: producer_ids).
by_producer.by_name.
ransack(params[:q]).result.
page(params[:page]).per(params[:per_page])
end
def render_paged_products(products)
def render_paged_products(products, product_serializer = ::Api::Admin::ProductSerializer)
serializer = ActiveModel::ArraySerializer.new(
products,
each_serializer: ::Api::Admin::ProductSerializer
each_serializer: product_serializer
)
render text: {

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
module Api
class ShopsController < BaseController
respond_to :json
skip_authorization_check only: [:show, :closed_shops]
def show
enterprise = Enterprise.find_by_id(params[:id])
render text: Api::EnterpriseShopfrontSerializer.new(enterprise).to_json, status: :ok
end
def closed_shops
@active_distributor_ids = []
@earliest_closing_times = []
serialized_closed_shops = ActiveModel::ArraySerializer.new(
ShopsListService.new.closed_shops,
each_serializer: Api::EnterpriseSerializer,
data: OpenFoodNetwork::EnterpriseInjectionData.new
)
render json: serialized_closed_shops
end
end
end

View File

@@ -4,13 +4,6 @@ class ShopsController < BaseController
before_filter :enable_embedded_shopfront
def index
@enterprises = Enterprise
.activated
.visible
.is_distributor
.includes(address: [:state, :country])
.includes(:properties)
.includes(supplied_products: :properties)
.all
@enterprises = ShopsListService.new.open_shops
end
end

View File

@@ -1,17 +1,9 @@
require 'open_food_network/subscription_payment_updater'
require 'open_food_network/subscription_summarizer'
require 'order_management/subscriptions/summarizer'
# Confirms orders of unconfirmed proxy orders in recently closed Order Cycles
class SubscriptionConfirmJob
def perform
ids = proxy_orders.pluck(:id)
proxy_orders.update_all(confirmed_at: Time.zone.now)
ProxyOrder.where(id: ids).each do |proxy_order|
Rails.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
@order = proxy_order.order
process!
end
send_confirmation_summary_emails
confirm_proxy_orders!
end
private
@@ -20,10 +12,26 @@ class SubscriptionConfirmJob
delegate :record_and_log_error, :send_confirmation_summary_emails, to: :summarizer
def summarizer
@summarizer ||= OpenFoodNetwork::SubscriptionSummarizer.new
@summarizer ||= OrderManagement::Subscriptions::Summarizer.new
end
def proxy_orders
def confirm_proxy_orders!
# Fetch all unconfirmed proxy orders
unconfirmed_proxy_orders_ids = unconfirmed_proxy_orders.pluck(:id)
# Mark these proxy orders as confirmed
unconfirmed_proxy_orders.update_all(confirmed_at: Time.zone.now)
# Confirm these proxy orders
ProxyOrder.where(id: unconfirmed_proxy_orders_ids).each do |proxy_order|
Rails.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
confirm_order!(proxy_order.order)
end
send_confirmation_summary_emails
end
def unconfirmed_proxy_orders
ProxyOrder.not_canceled.where('confirmed_at IS NULL AND placed_at IS NOT NULL')
.joins(:order_cycle).merge(recently_closed_order_cycles)
.joins(:order).merge(Spree::Order.complete.not_state('canceled'))
@@ -33,30 +41,43 @@ class SubscriptionConfirmJob
OrderCycle.closed.where('order_cycles.orders_close_at BETWEEN (?) AND (?) OR order_cycles.updated_at BETWEEN (?) AND (?)', 1.hour.ago, Time.zone.now, 1.hour.ago, Time.zone.now)
end
def process!
record_order(@order)
update_payment! if @order.payment_required?
return send_failed_payment_email if @order.errors.present?
# It sets up payments, processes payments and sends confirmation emails
def confirm_order!(order)
record_order(order)
@order.process_payments! if @order.payment_required?
return send_failed_payment_email if @order.errors.present?
send_confirm_email
if process_payment!(order)
send_confirmation_email(order)
else
send_failed_payment_email(order)
end
end
def update_payment!
OpenFoodNetwork::SubscriptionPaymentUpdater.new(@order).update!
def process_payment!(order)
return false if order.errors.present?
return true unless order.payment_required?
setup_payment!(order)
return false if order.errors.present?
order.process_payments!
return false if order.errors.present?
true
end
def send_confirm_email
@order.update!
record_success(@order)
SubscriptionMailer.confirmation_email(@order).deliver
def setup_payment!(order)
OrderManagement::Subscriptions::PaymentSetup.new(order).call!
end
def send_failed_payment_email
@order.update!
record_and_log_error(:failed_payment, @order)
SubscriptionMailer.failed_payment_email(@order).deliver
def send_confirmation_email(order)
order.update!
record_success(order)
SubscriptionMailer.confirmation_email(order).deliver
end
def send_failed_payment_email(order)
order.update!
record_and_log_error(:failed_payment, order)
SubscriptionMailer.failed_payment_email(order).deliver
end
end

View File

@@ -1,4 +1,4 @@
require 'open_food_network/subscription_summarizer'
require 'order_management/subscriptions/summarizer'
class SubscriptionPlacementJob
def perform
@@ -17,7 +17,7 @@ class SubscriptionPlacementJob
delegate :record_and_log_error, :send_placement_summary_emails, to: :summarizer
def summarizer
@summarizer ||= OpenFoodNetwork::SubscriptionSummarizer.new
@summarizer ||= OrderManagement::Subscriptions::Summarizer.new
end
def proxy_orders

View File

@@ -17,7 +17,7 @@ class OrderCycle < ActiveRecord::Base
has_many :distributors, source: :receiver, through: :cached_outgoing_exchanges, uniq: true
has_and_belongs_to_many :schedules, join_table: 'order_cycle_schedules'
has_paper_trail meta: { custom_data: :schedule_ids }
has_paper_trail meta: { custom_data: proc { |order_cycle| order_cycle.schedule_ids.to_s } }
attr_accessor :incoming_exchanges, :outgoing_exchanges

View File

@@ -1,6 +1,6 @@
class Schedule < ActiveRecord::Base
has_and_belongs_to_many :order_cycles, join_table: 'order_cycle_schedules'
has_paper_trail meta: { custom_data: :order_cycle_ids }
has_paper_trail meta: { custom_data: proc { |schedule| schedule.order_cycle_ids.to_s } }
has_many :coordinators, uniq: true, through: :order_cycles

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Spree
module Stock
class Quantifier
attr_reader :stock_items
def initialize(variant)
@variant = variant
@stock_items = fetch_stock_items
end
def total_on_hand
stock_items.sum(&:count_on_hand)
end
def backorderable?
stock_items.any?(&:backorderable)
end
def can_supply?(required)
total_on_hand >= required || backorderable?
end
private
def fetch_stock_items
# Don't re-fetch associated stock items from the DB if we've already eager-loaded them
return @variant.stock_items.to_a if @variant.stock_items.loaded?
Spree::StockItem.joins(:stock_location).
where(:variant_id => @variant, Spree::StockLocation.table_name => { active: true })
end
end
end
end

View File

@@ -15,8 +15,10 @@ class VariantOverrideSet < ModelSet
tag_list.empty?
end
# Override of ModelSet method to allow us to check presence of a tag_list (which is not an attribute)
# This method will delete VariantOverrides that have no values (see deletable? above)
# If the user sets all values to nil in the UI the VO will be deleted from the DB
def collection_to_delete
# Override of ModelSet method to allow us to check presence of a tag_list (which is not an attribute)
deleted = []
collection.delete_if { |e| deleted << e if @delete_if.andand.call(e.attributes, e.tag_list) }
deleted

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module Api
module Admin
class ProductSimpleSerializer < ActiveModel::Serializer
attributes :id, :name, :producer_id
has_many :variants, key: :variants, serializer: Api::Admin::VariantSimpleSerializer
def producer_id
object.supplier_id
end
end
end
end

View File

@@ -13,9 +13,9 @@ module Api
end
def in_open_and_upcoming_order_cycles
SubscriptionVariantsService.in_open_and_upcoming_order_cycles?(option_or_assigned_shop,
option_or_assigned_schedule,
object.variant)
OrderManagement::Subscriptions::VariantsList.in_open_and_upcoming_order_cycles?(option_or_assigned_shop,
option_or_assigned_schedule,
object.variant)
end
private

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Api
module Admin
class VariantSimpleSerializer < ActiveModel::Serializer
attributes :id, :name, :import_date,
:options_text, :unit_value, :unit_description, :unit_to_display,
:display_as, :display_name, :name_to_display,
:price, :on_demand, :on_hand
has_many :variant_overrides
def name
if object.full_name.present?
"#{object.name} - #{object.full_name}"
else
object.name
end
end
def on_hand
return 0 if object.on_hand.nil?
object.on_hand
end
def price
object.price.nil? ? 0.to_f : object.price
end
end
end
end

View File

@@ -73,12 +73,16 @@ module Api
# This results in 3 queries per enterprise
def distributed_properties
return [] unless active
(distributed_product_properties + distributed_producer_properties).uniq do |property_object|
property_object.property.presentation
end
end
def distributed_product_properties
return [] unless active
properties = Spree::Property
.joins(products: { variants: { exchanges: :order_cycle } })
.merge(Exchange.outgoing)
@@ -91,6 +95,8 @@ module Api
end
def distributed_producer_properties
return [] unless active
properties = Spree::Property
.joins(
producer_properties: {

View File

@@ -1,6 +1,6 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
require 'open_food_network/order_cycle_form_applicator'
require 'order_management/subscriptions/proxy_order_syncer'
class OrderCycleForm
def initialize(order_cycle, params, user)
@@ -58,7 +58,7 @@ class OrderCycleForm
return unless schedule_ids?
return unless schedule_sync_required?
OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
end
def schedule_sync_required?

View File

@@ -40,7 +40,7 @@ class ProductsRenderer
end
def product_scoper
OpenFoodNetwork::ScopeProductToHub.new(distributor)
@product_scoper ||= OpenFoodNetwork::ScopeProductToHub.new(distributor)
end
def enterprise_fee_calculator

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
class ShopsListService
def open_shops
shops_list.ready_for_checkout.all
end
def closed_shops
shops_list.not_ready_for_checkout.all
end
private
def shops_list
Enterprise
.activated
.visible
.is_distributor
.includes(address: [:state, :country])
.includes(:properties)
.includes(supplied_products: :properties)
end
end

View File

@@ -1,63 +0,0 @@
require 'open_food_network/scope_variant_to_hub'
# Responsible for estimating prices and fees for subscriptions
# Used by SubscriptionForm as part of the create/update process
# The values calculated here are intended to be persisted in the db
class SubscriptionEstimator
def initialize(subscription)
@subscription = subscription
end
def estimate!
assign_price_estimates
assign_fee_estimates
end
private
attr_accessor :subscription
delegate :subscription_line_items, :shipping_method, :payment_method, :shop, to: :subscription
def assign_price_estimates
subscription_line_items.each do |item|
item.price_estimate =
price_estimate_for(item.variant, item.price_estimate_was)
end
end
def price_estimate_for(variant, fallback)
return fallback unless fee_calculator && variant
scoper.scope(variant)
fees = fee_calculator.indexed_fees_for(variant)
(variant.price + fees).to_d
end
def fee_calculator
return @fee_calculator unless @fee_calculator.nil?
next_oc = subscription.schedule.andand.current_or_next_order_cycle
return nil unless shop && next_oc
@fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, next_oc)
end
def scoper
OpenFoodNetwork::ScopeVariantToHub.new(shop)
end
def assign_fee_estimates
subscription.shipping_fee_estimate = shipping_fee_estimate
subscription.payment_fee_estimate = payment_fee_estimate
end
def shipping_fee_estimate
shipping_method.calculator.compute(subscription)
end
def payment_fee_estimate
payment_method.calculator.compute(subscription)
end
end

View File

@@ -1,34 +0,0 @@
require 'open_food_network/proxy_order_syncer'
class SubscriptionForm
attr_accessor :subscription, :params, :order_update_issues, :validator, :order_syncer, :estimator
delegate :json_errors, :valid?, to: :validator
delegate :order_update_issues, to: :order_syncer
def initialize(subscription, params = {})
@subscription = subscription
@params = params
@estimator = SubscriptionEstimator.new(subscription)
@validator = SubscriptionValidator.new(subscription)
@order_syncer = OrderSyncer.new(subscription)
end
def save
subscription.assign_attributes(params)
return false unless valid?
subscription.transaction do
estimator.estimate!
proxy_order_syncer.sync!
order_syncer.sync!
subscription.save!
end
end
private
def proxy_order_syncer
OpenFoodNetwork::ProxyOrderSyncer.new(subscription)
end
end

View File

@@ -1,127 +0,0 @@
# Encapsulation of all of the validation logic required for subscriptions
# Public interface consists of #valid? method provided by ActiveModel::Validations
# and #json_errors which compiles a serializable hash of errors
class SubscriptionValidator
include ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :subscription
validates :shop, :customer, :schedule, :shipping_method, :payment_method, presence: true
validates :bill_address, :ship_address, :begins_at, presence: true
validate :shipping_method_allowed?
validate :payment_method_allowed?
validate :payment_method_type_allowed?
validate :ends_at_after_begins_at?
validate :customer_allowed?
validate :schedule_allowed?
validate :credit_card_ok?
validate :subscription_line_items_present?
validate :requested_variants_available?
delegate :shop, :customer, :schedule, :shipping_method, :payment_method, to: :subscription
delegate :bill_address, :ship_address, :begins_at, :ends_at, to: :subscription
delegate :subscription_line_items, to: :subscription
def initialize(subscription)
@subscription = subscription
end
def json_errors
errors.messages.each_with_object({}) do |(k, v), errors|
errors[k] = v.map { |msg| build_msg_from(k, msg) }
end
end
private
def shipping_method_allowed?
return unless shipping_method
return if shipping_method.distributors.include?(shop)
errors.add(:shipping_method, :not_available_to_shop, shop: shop.name)
end
def payment_method_allowed?
return unless payment_method
return if payment_method.distributors.include?(shop)
errors.add(:payment_method, :not_available_to_shop, shop: shop.name)
end
def payment_method_type_allowed?
return unless payment_method
return if Subscription::ALLOWED_PAYMENT_METHOD_TYPES.include? payment_method.type
errors.add(:payment_method, :invalid_type)
end
def ends_at_after_begins_at?
# Only validates ends_at if it is present
return if begins_at.blank? || ends_at.blank?
return if ends_at > begins_at
errors.add(:ends_at, :after_begins_at)
end
def customer_allowed?
return unless customer
return if customer.enterprise == shop
errors.add(:customer, :does_not_belong_to_shop, shop: shop.name)
end
def schedule_allowed?
return unless schedule
return if schedule.coordinators.include?(shop)
errors.add(:schedule, :not_coordinated_by_shop, shop: shop.name)
end
def credit_card_ok?
return unless customer && payment_method
return unless stripe_payment_method?(payment_method)
return errors.add(:payment_method, :charges_not_allowed) unless customer.allow_charges
return if customer.user.andand.default_card.present?
errors.add(:payment_method, :no_default_card)
end
def stripe_payment_method?(payment_method)
payment_method.type == "Spree::Gateway::StripeConnect" ||
payment_method.type == "Spree::Gateway::StripeSCA"
end
def subscription_line_items_present?
return if subscription_line_items.reject(&:marked_for_destruction?).any?
errors.add(:subscription_line_items, :at_least_one_product)
end
def requested_variants_available?
subscription_line_items.each { |sli| verify_availability_of(sli.variant) }
end
def verify_availability_of(variant)
return if available_variant_ids.include? variant.id
name = "#{variant.product.name} - #{variant.full_name}"
errors.add(:subscription_line_items, :not_available, name: name)
end
def available_variant_ids
return @available_variant_ids if @available_variant_ids.present?
subscription_variant_ids = subscription_line_items.map(&:variant_id)
@available_variant_ids = SubscriptionVariantsService.eligible_variants(shop)
.where(id: subscription_variant_ids).pluck(:id)
end
def build_msg_from(k, msg)
return msg[1..-1] if msg.starts_with?("^")
errors.full_message(k, msg)
end
end

View File

@@ -1,39 +0,0 @@
class SubscriptionVariantsService
# Includes the following variants:
# - Variants of permitted producers
# - Variants of hub
# - Variants that are in outgoing exchanges where the hub is receiver
def self.eligible_variants(distributor)
variant_conditions = ["spree_products.supplier_id IN (?)", permitted_producer_ids(distributor)]
exchange_variant_ids = outgoing_exchange_variant_ids(distributor)
if exchange_variant_ids.present?
variant_conditions[0] << " OR spree_variants.id IN (?)"
variant_conditions << exchange_variant_ids
end
Spree::Variant.joins(:product).where(is_master: false).where(*variant_conditions)
end
def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant)
scope = ExchangeVariant.joins(exchange: { order_cycle: :schedules })
.where(variant_id: variant, exchanges: { incoming: false, receiver_id: distributor })
.merge(OrderCycle.not_closed)
scope = scope.where(schedules: { id: schedule })
scope.any?
end
def self.permitted_producer_ids(distributor)
other_permitted_producer_ids = EnterpriseRelationship.joins(:parent)
.permitting(distributor.id).with_permission(:add_to_order_cycle)
.merge(Enterprise.is_primary_producer)
.pluck(:parent_id)
other_permitted_producer_ids | [distributor.id]
end
def self.outgoing_exchange_variant_ids(distributor)
ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange)
.where(exchanges: { incoming: false, receiver_id: distributor.id })
.pluck(:variant_id)
end
end

View File

@@ -1,20 +0,0 @@
class SubscriptionsCount
def initialize(order_cycles)
@order_cycles = order_cycles
end
def for(order_cycle_id)
active[order_cycle_id] || 0
end
private
attr_accessor :order_cycles
def active
return @active unless @active.nil?
return @active = [] if order_cycles.blank?
@active ||= ProxyOrder.not_canceled.group(:order_cycle_id).where(order_cycle_id: order_cycles).count
end
end

View File

@@ -8,10 +8,13 @@ class VariantsStockLevels
variant_stock_levels = variant_stock_levels(order.line_items)
order_variant_ids = variant_stock_levels.keys
missing_variant_ids = requested_variant_ids - order_variant_ids
missing_variant_ids.each do |variant_id|
variant = scoped_variant(order.distributor, Spree::Variant.find(variant_id))
variant_stock_levels[variant_id] = { quantity: 0, max_quantity: 0, on_hand: variant.on_hand, on_demand: variant.on_demand }
missing_variants = Spree::Variant.includes(:stock_items).
where(id: (requested_variant_ids - order_variant_ids))
missing_variants.each do |missing_variant|
variant = scoped_variant(order.distributor, missing_variant)
variant_stock_levels[variant.id] =
{ quantity: 0, max_quantity: 0, on_hand: variant.on_hand, on_demand: variant.on_demand }
end
variant_stock_levels

View File

@@ -26,8 +26,11 @@
%a{href: "", "ng-click" => "showDistanceMatches()"}
= t :hubs_distance_filter, location: "{{ nameMatchesFiltered[0].name }}"
.more-controls
%a.button{href: "", ng: {click: "showClosedShops()", show: "!show_closed"}}
= t '.show_closed_shops'
%a.button{href: "", ng: {click: "hideClosedShops()", show: "show_closed"}}
= t '.hide_closed_shops'
%img.spinner.text-center{ng: {show: "closed_shops_loading"}, src: "/assets/spinning-circles.svg" }
%span{ng: {if: "!show_closed", cloak: true}}
%a.button{href: "", ng: {click: "showClosedShops()"}}
= t '.show_closed_shops'
%span{ng: {if: "show_closed", cloak: true}}
%a.button{href: "", ng: {click: "hideClosedShops()"}}
= t '.hide_closed_shops'
%a.button{href: main_app.map_path}= t '.show_on_map'

View File

@@ -13,7 +13,8 @@
%tr.order-row
%td.order1
%a{"ng-href" => "{{::order.path}}", "ng-bind" => "::order.number"}
%td.order2{"ng-bind" => "::Orders.shopsByID[order.shop_id].name"}
%td.order2
%a{"ng-href" => "{{::Orders.shopsByID[order.shop_id].hash}}#{main_app.shop_path}", "ng-bind" => "::Orders.shopsByID[order.shop_id].name"}
%td.order3.show-for-large-up{"ng-bind" => "::order.changes_allowed_until"}
%td.order4.show-for-large-up{"ng-bind" => "::order.item_count"}
%td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","ng-bind" => "::order.total | localizeCurrency"}

View File

@@ -13,7 +13,8 @@
%tr.order-row
%td.order1
%a{"ng-href" => "{{::order.path}}", "ng-bind" => "::order.number"}
%td.order2{"ng-bind" => "::Orders.shopsByID[order.shop_id].name"}
%td.order2
%a{"ng-href" => "{{::Orders.shopsByID[order.shop_id].hash}}#{main_app.shop_path}", "ng-bind" => "::Orders.shopsByID[order.shop_id].name"}
%td.order3.show-for-large-up{"ng-bind" => "::order.completed_at"}
%td.order4.show-for-large-up{"ng-bind" => "::order.item_count"}
%td.order5.text-right{"ng-class" => "{'debit': order.payment_state != 'paid', 'credit': order.payment_state == 'paid'}","ng-bind" => "::order.total | localizeCurrency"}

View File

@@ -1,4 +1,17 @@
# This file is used by Rack-based servers to start the application.
if ENV.fetch('KILL_UNICORNS', false) && ['production', 'staging'].include?(ENV['RAILS_ENV'])
# Gracefully restart individual unicorn workers if they have:
# - performed between 25000 and 30000 requests
# - grown in memory usage to between 700 and 850 MB
require 'unicorn/worker_killer'
use Unicorn::WorkerKiller::MaxRequests,
ENV.fetch('UWK_REQS_MIN', 25_000).to_i,
ENV.fetch('UWK_REQS_MAX', 30_000).to_i
use Unicorn::WorkerKiller::Oom,
( ENV.fetch('UWK_MEM_MIN', 700).to_i * (1024**2) ),
( ENV.fetch('UWK_MEM_MAX', 850).to_i * (1024**2) )
end
require ::File.expand_path('../config/environment', __FILE__)
run Openfoodnetwork::Application

View File

@@ -1,7 +1,7 @@
defaults: &defaults
adapter: postgresql
encoding: unicode
pool: 5
pool: <%= ENV.fetch('OFN_DB_POOL', 5) %>
host: <%= ENV.fetch('OFN_DB_HOST', 'localhost') %>
username: <%= ENV.fetch('OFN_DB_USERNAME', 'ofn') %>
password: <%= ENV.fetch('OFN_DB_PASSWORD', 'f00d') %>

View File

@@ -4,5 +4,6 @@ if ENV['DATADOG_RAILS_APM']
c.use :delayed_job, service_name: 'delayed_job'
c.use :dalli, service_name: 'memcached'
c.analytics_enabled = true
c.runtime_metrics_enabled = true
end
end

View File

@@ -1,138 +0,0 @@
# Use this setup block to configure all options available in SimpleForm.
SimpleForm.setup do |config|
# Wrappers are used by the form builder to generate a
# complete input. You can remove any component from the
# wrapper, change the order or even add your own to the
# stack. The options given below are used to wrap the
# whole input.
config.wrappers :default, :class => :input,
:hint_class => :field_with_hint, :error_class => :field_with_errors do |b|
## Extensions enabled by default
# Any of these extensions can be disabled for a
# given input by passing: `f.input EXTENSION_NAME => false`.
# You can make any of these extensions optional by
# renaming `b.use` to `b.optional`.
# Determines whether to use HTML5 (:email, :url, ...)
# and required attributes
b.use :html5
# Calculates placeholders automatically from I18n
# You can also pass a string as f.input :placeholder => "Placeholder"
b.use :placeholder
## Optional extensions
# They are disabled unless you pass `f.input EXTENSION_NAME => :lookup`
# to the input. If so, they will retrieve the values from the model
# if any exists. If you want to enable the lookup for any of those
# extensions by default, you can change `b.optional` to `b.use`.
# Calculates maxlength from length validations for string inputs
b.optional :maxlength
# Calculates pattern from format validations for string inputs
b.optional :pattern
# Calculates min and max from length validations for numeric inputs
b.optional :min_max
# Calculates readonly automatically from readonly attributes
b.optional :readonly
## Inputs
b.use :label_input
b.use :hint, :wrap_with => { :tag => :span, :class => :hint }
b.use :error, :wrap_with => { :tag => :span, :class => :error }
end
# The default wrapper to be used by the FormBuilder.
config.default_wrapper = :default
# Define the way to render check boxes / radio buttons with labels.
# Defaults to :nested for bootstrap config.
# :inline => input + label
# :nested => label > input
config.boolean_style = :nested
# Default class for buttons
config.button_class = 'btn'
# Method used to tidy up errors. Specify any Rails Array method.
# :first lists the first message for each field.
# Use :to_sentence to list all errors for each field.
# config.error_method = :first
# Default tag used for error notification helper.
config.error_notification_tag = :div
# CSS class to add for error notification helper.
config.error_notification_class = 'alert alert-error'
# ID to add for error notification helper.
# config.error_notification_id = nil
# Series of attempts to detect a default label method for collection.
# config.collection_label_methods = [ :to_label, :name, :title, :to_s ]
# Series of attempts to detect a default value method for collection.
# config.collection_value_methods = [ :id, :to_s ]
# You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none.
# config.collection_wrapper_tag = nil
# You can define the class to use on all collection wrappers. Defaulting to none.
# config.collection_wrapper_class = nil
# You can wrap each item in a collection of radio/check boxes with a tag,
# defaulting to :span. Please note that when using :boolean_style = :nested,
# SimpleForm will force this option to be a label.
# config.item_wrapper_tag = :span
# You can define a class to use in all item wrappers. Defaulting to none.
# config.item_wrapper_class = nil
# How the label text should be generated altogether with the required text.
# config.label_text = lambda { |label, required| "#{required} #{label}" }
# You can define the class to use on all labels. Default is nil.
config.label_class = 'control-label'
# You can define the class to use on all forms. Default is simple_form.
# config.form_class = :simple_form
# You can define which elements should obtain additional classes
# config.generate_additional_classes_for = [:wrapper, :label, :input]
# Whether attributes are required by default (or not). Default is true.
# config.required_by_default = true
# Tell browsers whether to use default HTML5 validations (novalidate option).
# Default is enabled.
config.browser_validations = false
# Collection of methods to detect if a file type was given.
# config.file_methods = [ :mounted_as, :file?, :public_filename ]
# Custom mappings for input types. This should be a hash containing a regexp
# to match as key, and the input type that will be used when the field name
# matches the regexp as value.
# config.input_mappings = { /count/ => :integer }
# Default priority for time_zone inputs.
# config.time_zone_priority = nil
# Default priority for country inputs.
# config.country_priority = nil
# Default size for text inputs.
# config.default_input_size = 50
# When false, do not use translations for labels.
# config.translate_labels = true
# Automatically discover new inputs in Rails' autoload path.
# config.inputs_discovery = true
# Cache SimpleForm inputs discovery
# config.cache_discovery = !Rails.env.development?
end

View File

@@ -55,7 +55,7 @@ ar:
messages:
inclusion: "غير مدرجة في القائمة"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^ الرجاء إضافة منتج واحد على الأقل"

View File

@@ -31,6 +31,10 @@ ca:
taken: "Ja hi ha un compte per a aquest correu electrònic. Si us plau, inicia sessió o restableix la contrasenya."
spree/order:
no_card: No hi ha targetes de crèdit autoritzades disponibles per carregar
spree/credit_card:
attributes:
base:
card_expired: "Ha expirat"
order_cycle:
attributes:
orders_close_at:
@@ -55,7 +59,7 @@ ca:
messages:
inclusion: "no està inclòs a la llista"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Afegiu com a mínim un producte"

View File

@@ -59,7 +59,7 @@ de_DE:
messages:
inclusion: "ist in der Liste nicht enthalten"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^ Bitte fügen Sie mindestens ein Produkt hinzu"

View File

@@ -80,7 +80,7 @@ en:
messages:
inclusion: "is not included in the list"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -53,7 +53,7 @@ en_AU:
payment_method_ids: "Payment Methods"
errors:
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -53,7 +53,7 @@ en_BE:
payment_method_ids: "Payment Methods"
errors:
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -55,7 +55,7 @@ en_CA:
messages:
inclusion: "is not included in the list"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -53,7 +53,7 @@ en_DE:
payment_method_ids: "Payment Methods"
errors:
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -59,7 +59,7 @@ en_FR:
messages:
inclusion: "is not included in the list"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -31,6 +31,10 @@ en_GB:
taken: "There's already an account for this email. Please login or reset your password."
spree/order:
no_card: There are no authorised credit cards available to charge
spree/credit_card:
attributes:
base:
card_expired: "has expired"
order_cycle:
attributes:
orders_close_at:
@@ -55,7 +59,7 @@ en_GB:
messages:
inclusion: "is not included in the list"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -31,6 +31,10 @@ en_NZ:
taken: "There's already an account for this email. Please login or reset your password."
spree/order:
no_card: There are no authorised credit cards available to charge
spree/credit_card:
attributes:
base:
card_expired: "has expired"
order_cycle:
attributes:
orders_close_at:
@@ -55,7 +59,7 @@ en_NZ:
messages:
inclusion: "is not included in the list"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"
@@ -867,6 +871,7 @@ en_NZ:
distributor: "Distributor"
products: "Products"
tags: "Tags"
delivery_details: "Delivery Details"
fees: "Fees"
previous: "Previous"
save: "Save"

3373
config/locales/en_PH.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -55,7 +55,7 @@ en_US:
messages:
inclusion: "is not included in the list"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -53,7 +53,7 @@ en_ZA:
payment_method_ids: "Payment Methods"
errors:
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"

View File

@@ -53,7 +53,7 @@ es:
payment_method_ids: "Métodos de Pago"
errors:
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Por favor agrega al menos un producto"

3385
config/locales/fil_PH.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -59,7 +59,7 @@ fr:
messages:
inclusion: "n'est pas inclus dans la liste"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Veuillez ajouter au moins un produit"
@@ -1670,7 +1670,7 @@ fr:
orders_form_admin: Admin & traitements
orders_form_total: Total
orders_oc_expired_headline: Les commandes ne sont plus possibles pour ce cycle de vente.
orders_oc_expired_text: "Désolé, les commandes pour ce cycle de vente ont été clôturées il y a %{time}! Veuillez contacter directement le hub pour voir s'il accepte les commandes tardives."
orders_oc_expired_text: "Désolé, les commandes pour ce cycle de vente ont été clôturées il y a %{time}!"
orders_oc_expired_text_others_html: "Désolé, les commandes pour ce cycle de vente ont été clôturées il y a %{time}! Veuillez contacter directement le hub pour voir s'il accepte les commandes tardives <strong>%{link}</strong>."
orders_oc_expired_text_link: "ou voir si d'autres cycles de vente sont ouverts pour ce hub"
orders_oc_expired_email: "Email:"

View File

@@ -55,7 +55,7 @@ fr_BE:
messages:
inclusion: "N'est pas inclus dans la liste"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Veuillez ajouter au moins un produit"

View File

@@ -55,7 +55,7 @@ fr_CA:
messages:
inclusion: "n'est pas inclus dans la liste"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "Veuillez ajouter au moins un produit"

View File

@@ -55,7 +55,7 @@ it:
messages:
inclusion: "non incluso nella lista"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Aggiungi almeno un prodotto"

View File

@@ -55,7 +55,7 @@ nb:
messages:
inclusion: "er ikke inkludert i listen"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Vennligst legg til minst ett produkt"

View File

@@ -53,7 +53,7 @@ nl_BE:
payment_method_ids: "Betaalmethodes"
errors:
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Gelieve minstens één product toe te voegen"

View File

@@ -51,7 +51,7 @@ pt:
payment_method_ids: "Métodos de pagamento"
errors:
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Por favor adicione pelo menos um produto"

View File

@@ -53,7 +53,7 @@ pt_BR:
messages:
inclusion: "não está incluso na lista"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^ Por favor, adicione pelo menos um produto"

View File

@@ -1,25 +0,0 @@
en:
simple_form:
"yes": 'Yes'
"no": 'No'
required:
text: 'required'
mark: '*'
# You can uncomment the line below if you need to overwrite the whole required html.
# When using html, text and mark won't be used.
# html: '<abbr title="required">*</abbr>'
error_notification:
default_message: "Please review the problems below:"
# Labels and hints examples
# labels:
# defaults:
# password: 'Password'
# user:
# new:
# email: 'E-mail to sign in.'
# edit:
# email: 'E-mail.'
# hints:
# defaults:
# username: 'User name to sign in.'
# password: 'No special characters, please.'

View File

@@ -55,7 +55,7 @@ tr:
messages:
inclusion: "Listeye dahil değil"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^ Lütfen en az bir ürün ekleyin"

View File

@@ -33,9 +33,11 @@ Openfoodnetwork::Application.routes.draw do
resource :logo, only: [:destroy]
resource :promo_image, only: [:destroy]
end
member do
get :shopfront
resources :shops, only: [:show] do
collection do
get :closed_shops
end
end

View File

@@ -28,10 +28,7 @@ services:
ADMIN_PASSWORD: ofn123
OFN_DB_HOST: db
command: >
bash -c "(bundle check || bundle install) &&
wait-for-it -t 30 db:5432 &&
bundle exec rake db:reset &&
bundle exec rake db:test:prepare ofn:sample_data || true &&
bash -c "wait-for-it -t 30 db:5432 &&
rm -f tmp/pids/server.pid &&
bundle exec rails s -p 3000 -b '0.0.0.0'"

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
class Count
def initialize(order_cycles)
@order_cycles = order_cycles
end
def for(order_cycle_id)
active[order_cycle_id] || 0
end
private
attr_accessor :order_cycles
def active
return @active unless @active.nil?
return @active = [] if order_cycles.blank?
@active ||= ProxyOrder.
not_canceled.
group(:order_cycle_id).
where(order_cycle_id: order_cycles).
count
end
end
end
end

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'open_food_network/scope_variant_to_hub'
# Responsible for estimating prices and fees for subscriptions
# Used by Form as part of the create/update process
# The values calculated here are intended to be persisted in the db
module OrderManagement
module Subscriptions
class Estimator
def initialize(subscription)
@subscription = subscription
end
def estimate!
assign_price_estimates
assign_fee_estimates
end
private
attr_accessor :subscription
delegate :subscription_line_items, :shipping_method, :payment_method, :shop, to: :subscription
def assign_price_estimates
subscription_line_items.each do |item|
item.price_estimate =
price_estimate_for(item.variant, item.price_estimate_was)
end
end
def price_estimate_for(variant, fallback)
return fallback unless fee_calculator && variant
scoper.scope(variant)
fees = fee_calculator.indexed_fees_for(variant)
(variant.price + fees).to_d
end
def fee_calculator
return @fee_calculator unless @fee_calculator.nil?
next_oc = subscription.schedule.andand.current_or_next_order_cycle
return nil unless shop && next_oc
@fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, next_oc)
end
def scoper
OpenFoodNetwork::ScopeVariantToHub.new(shop)
end
def assign_fee_estimates
subscription.shipping_fee_estimate = shipping_fee_estimate
subscription.payment_fee_estimate = payment_fee_estimate
end
def shipping_fee_estimate
shipping_method.calculator.compute(subscription)
end
def payment_fee_estimate
payment_method.calculator.compute(subscription)
end
end
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'order_management/subscriptions/proxy_order_syncer'
module OrderManagement
module Subscriptions
class Form
attr_accessor :subscription, :params, :order_update_issues,
:validator, :order_syncer, :estimator
delegate :json_errors, :valid?, to: :validator
delegate :order_update_issues, to: :order_syncer
def initialize(subscription, params = {})
@subscription = subscription
@params = params
@estimator = OrderManagement::Subscriptions::Estimator.new(subscription)
@validator = OrderManagement::Subscriptions::Validator.new(subscription)
@order_syncer = OrderSyncer.new(subscription)
end
def save
subscription.assign_attributes(params)
return false unless valid?
subscription.transaction do
estimator.estimate!
proxy_order_syncer.sync!
order_syncer.sync!
subscription.save!
end
end
private
def proxy_order_syncer
OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscription)
end
end
end
end

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
class PaymentSetup
def initialize(order)
@order = order
end
def call!
create_payment
ensure_payment_source
return if order.errors.any?
payment.update_attributes(amount: order.outstanding_balance)
end
private
attr_reader :order
def payment
@payment ||= order.pending_payments.last
end
def create_payment
return if payment.present?
@payment = order.payments.create(
payment_method_id: order.subscription.payment_method_id,
amount: order.outstanding_balance
)
end
def card_required?
[Spree::Gateway::StripeConnect,
Spree::Gateway::StripeSCA].include? payment.payment_method.class
end
def card_set?
payment.source is_a? Spree::CreditCard
end
def ensure_payment_source
return unless card_required? && !card_set?
ensure_credit_card || order.errors.add(:base, :no_card)
end
def ensure_credit_card
return false if saved_credit_card.blank? || !allow_charges?
payment.update_attributes(source: saved_credit_card)
end
def allow_charges?
order.customer.allow_charges?
end
def saved_credit_card
order.user.default_card
end
def errors_present?
order.errors.any?
end
end
end
end

View File

@@ -0,0 +1,99 @@
# frozen_string_literal: false
module OrderManagement
module Subscriptions
class ProxyOrderSyncer
attr_reader :subscription
delegate :order_cycles, :proxy_orders, :begins_at, :ends_at, to: :subscription
def initialize(subscriptions)
case subscriptions
when Subscription
@subscription = subscriptions
when ActiveRecord::Relation
@subscriptions = subscriptions.not_ended.not_canceled
else
raise "ProxyOrderSyncer must be initialized with " \
"an instance of Subscription or ActiveRecord::Relation"
end
end
def sync!
return sync_subscriptions! if @subscriptions
return initialise_proxy_orders! unless @subscription.id
sync_subscription!
end
private
def sync_subscriptions!
@subscriptions.each do |subscription|
@subscription = subscription
sync_subscription!
end
end
def initialise_proxy_orders!
uninitialised_order_cycle_ids.each do |order_cycle_id|
Rails.logger.info "Initializing Proxy Order " \
"of subscription #{@subscription.id} in order cycle #{order_cycle_id}"
proxy_orders << ProxyOrder.new(subscription: subscription, order_cycle_id: order_cycle_id)
end
end
def sync_subscription!
Rails.logger.info "Syncing Proxy Orders of subscription #{@subscription.id}"
create_proxy_orders!
remove_orphaned_proxy_orders!
end
def create_proxy_orders!
return unless not_closed_in_range_order_cycles.any?
query = "INSERT INTO proxy_orders (subscription_id, order_cycle_id, updated_at, created_at)"
query << " VALUES #{insert_values}"
query << " ON CONFLICT DO NOTHING"
ActiveRecord::Base.connection.exec_query(query)
end
def uninitialised_order_cycle_ids
not_closed_in_range_order_cycles.pluck(:id) - proxy_orders.map(&:order_cycle_id)
end
def remove_orphaned_proxy_orders!
orphaned_proxy_orders.scoped.delete_all
end
# Remove Proxy Orders that have not been placed yet
# and are in Order Cycles that are out of range
def orphaned_proxy_orders
orphaned = proxy_orders.where(placed_at: nil)
order_cycle_ids = in_range_order_cycles.pluck(:id)
return orphaned unless order_cycle_ids.any?
orphaned.where('order_cycle_id NOT IN (?)', order_cycle_ids)
end
def insert_values
now = Time.now.utc.iso8601
not_closed_in_range_order_cycles
.map{ |oc| "(#{subscription.id},#{oc.id},'#{now}','#{now}')" }
.join(",")
end
def not_closed_in_range_order_cycles
in_range_order_cycles.merge(OrderCycle.not_closed)
end
def in_range_order_cycles
order_cycles.where("orders_close_at >= ? AND orders_close_at <= ?",
begins_at,
ends_at || 100.years.from_now)
end
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
# Used by for SubscriptionPlacementJob and SubscriptionConfirmJob to summarize the
# result of automatic processing of subscriptions for the relevant shop owners.
module OrderManagement
module Subscriptions
class Summarizer
def initialize
@summaries = {}
end
def record_order(order)
summary_for(order).record_order(order)
end
def record_success(order)
summary_for(order).record_success(order)
end
def record_issue(type, order, message = nil)
Rails.logger.info "Issue in Subscription Order #{order.id}: #{type}"
summary_for(order).record_issue(type, order, message)
end
def record_and_log_error(type, order)
return record_issue(type, order) unless order.errors.any?
error = "Subscription#{type.to_s.camelize}Error"
line1 = "#{error}: Cannot process order #{order.number} due to errors"
line2 = "Errors: #{order.errors.full_messages.join(', ')}"
Rails.logger.info("#{line1}\n#{line2}")
record_issue(type, order, line2)
end
def send_placement_summary_emails
@summaries.values.each do |summary|
SubscriptionMailer.placement_summary_email(summary).deliver
end
end
def send_confirmation_summary_emails
@summaries.values.each do |summary|
SubscriptionMailer.confirmation_summary_email(summary).deliver
end
end
private
def summary_for(order)
shop_id = order.distributor_id
@summaries[shop_id] ||= Summary.new(shop_id)
end
end
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
class Summary
attr_reader :shop_id, :issues
def initialize(shop_id)
@shop_id = shop_id
@order_ids = []
@success_ids = []
@issues = {}
end
def record_order(order)
@order_ids << order.id
end
def record_success(order)
@success_ids << order.id
end
def record_issue(type, order, message)
issues[type] ||= {}
issues[type][order.id] = message
end
def order_count
@order_ids.count
end
def success_count
@success_ids.count
end
def issue_count
(@order_ids - @success_ids).count
end
def orders_affected_by(type)
case type
when :other then Spree::Order.where(id: unrecorded_ids)
else Spree::Order.where(id: issues[type].keys)
end
end
def unrecorded_ids
recorded_ids = issues.values.map(&:keys).flatten
@order_ids - @success_ids - recorded_ids
end
end
end
end

View File

@@ -0,0 +1,132 @@
# frozen_string_literal: true
# Encapsulation of all of the validation logic required for subscriptions
# Public interface consists of #valid? method provided by ActiveModel::Validations
# and #json_errors which compiles a serializable hash of errors
module OrderManagement
module Subscriptions
class Validator
include ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :subscription
validates :shop, :customer, :schedule, :shipping_method, :payment_method, presence: true
validates :bill_address, :ship_address, :begins_at, presence: true
validate :shipping_method_allowed?
validate :payment_method_allowed?
validate :payment_method_type_allowed?
validate :ends_at_after_begins_at?
validate :customer_allowed?
validate :schedule_allowed?
validate :credit_card_ok?
validate :subscription_line_items_present?
validate :requested_variants_available?
delegate :shop, :customer, :schedule, :shipping_method, :payment_method, to: :subscription
delegate :bill_address, :ship_address, :begins_at, :ends_at, to: :subscription
delegate :subscription_line_items, to: :subscription
def initialize(subscription)
@subscription = subscription
end
def json_errors
errors.messages.each_with_object({}) do |(key, value), errors|
errors[key] = value.map { |msg| build_msg_from(key, msg) }
end
end
private
def shipping_method_allowed?
return unless shipping_method
return if shipping_method.distributors.include?(shop)
errors.add(:shipping_method, :not_available_to_shop, shop: shop.name)
end
def payment_method_allowed?
return unless payment_method
return if payment_method.distributors.include?(shop)
errors.add(:payment_method, :not_available_to_shop, shop: shop.name)
end
def payment_method_type_allowed?
return unless payment_method
return if Subscription::ALLOWED_PAYMENT_METHOD_TYPES.include? payment_method.type
errors.add(:payment_method, :invalid_type)
end
def ends_at_after_begins_at?
# Only validates ends_at if it is present
return if begins_at.blank? || ends_at.blank?
return if ends_at > begins_at
errors.add(:ends_at, :after_begins_at)
end
def customer_allowed?
return unless customer
return if customer.enterprise == shop
errors.add(:customer, :does_not_belong_to_shop, shop: shop.name)
end
def schedule_allowed?
return unless schedule
return if schedule.coordinators.include?(shop)
errors.add(:schedule, :not_coordinated_by_shop, shop: shop.name)
end
def credit_card_ok?
return unless customer && payment_method
return unless stripe_payment_method?(payment_method)
return errors.add(:payment_method, :charges_not_allowed) unless customer.allow_charges
return if customer.user.andand.default_card.present?
errors.add(:payment_method, :no_default_card)
end
def stripe_payment_method?(payment_method)
payment_method.type == "Spree::Gateway::StripeConnect" ||
payment_method.type == "Spree::Gateway::StripeSCA"
end
def subscription_line_items_present?
return if subscription_line_items.reject(&:marked_for_destruction?).any?
errors.add(:subscription_line_items, :at_least_one_product)
end
def requested_variants_available?
subscription_line_items.each { |sli| verify_availability_of(sli.variant) }
end
def verify_availability_of(variant)
return if available_variant_ids.include? variant.id
name = "#{variant.product.name} - #{variant.full_name}"
errors.add(:subscription_line_items, :not_available, name: name)
end
def available_variant_ids
return @available_variant_ids if @available_variant_ids.present?
subscription_variant_ids = subscription_line_items.map(&:variant_id)
@available_variant_ids = OrderManagement::Subscriptions::VariantsList.eligible_variants(shop)
.where(id: subscription_variant_ids).pluck(:id)
end
def build_msg_from(key, msg)
return msg[1..-1] if msg.starts_with?("^")
errors.full_message(key, msg)
end
end
end
end

View File

@@ -0,0 +1,46 @@
# frozen_string_literal: false
module OrderManagement
module Subscriptions
class VariantsList
# Includes the following variants:
# - Variants of permitted producers
# - Variants of hub
# - Variants that are in outgoing exchanges where the hub is receiver
def self.eligible_variants(distributor)
variant_conditions = ["spree_products.supplier_id IN (?)",
permitted_producer_ids(distributor)]
exchange_variant_ids = outgoing_exchange_variant_ids(distributor)
if exchange_variant_ids.present?
variant_conditions[0] << " OR spree_variants.id IN (?)"
variant_conditions << exchange_variant_ids
end
Spree::Variant.joins(:product).where(is_master: false).where(*variant_conditions)
end
def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant)
scope = ExchangeVariant.joins(exchange: { order_cycle: :schedules })
.where(variant_id: variant, exchanges: { incoming: false, receiver_id: distributor })
.merge(OrderCycle.not_closed)
scope = scope.where(schedules: { id: schedule })
scope.any?
end
def self.permitted_producer_ids(distributor)
other_permitted_producer_ids = EnterpriseRelationship.joins(:parent)
.permitting(distributor.id).with_permission(:add_to_order_cycle)
.merge(Enterprise.is_primary_producer)
.pluck(:parent_id)
other_permitted_producer_ids | [distributor.id]
end
def self.outgoing_exchange_variant_ids(distributor)
ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange)
.where(exchanges: { incoming: false, receiver_id: distributor.id })
.pluck(:variant_id)
end
end
end
end

View File

@@ -0,0 +1,69 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
describe ProxyOrderSyncer, performance: true do
let(:start) { Time.zone.now.beginning_of_day }
let!(:schedule) { create(:schedule, order_cycles: order_cycles) }
let!(:order_cycles) do
Array.new(10) do |i|
create(:simple_order_cycle, orders_open_at: start + i.days,
orders_close_at: start + (i + 1).days )
end
end
let!(:subscriptions) do
Array.new(150) do |_i|
create(:subscription, schedule: schedule, begins_at: start, ends_at: start + 10.days)
end
Subscription.where(schedule_id: schedule)
end
context "measuring performance for initialisation" do
it "reports the average run time for adding 10 OCs to 150 subscriptions" do
expect(ProxyOrder.count).to be 0
times = []
10.times do
syncer = ProxyOrderSyncer.new(subscriptions.reload)
t1 = Time.zone.now
syncer.sync!
t2 = Time.zone.now
diff = t2 - t1
times << diff
puts diff.round(2)
expect(ProxyOrder.count).to be 1500
ProxyOrder.destroy_all
end
puts "AVG: #{(times.sum / times.count).round(2)}"
end
end
context "measuring performance for removal" do
it "reports the average run time for removing 8 OCs from 150 subscriptions" do
times = []
10.times do
syncer = ProxyOrderSyncer.new(subscriptions.reload)
syncer.sync!
expect(ProxyOrder.count).to be 1500
subscriptions.update_all(begins_at: start + 8.days + 1.minute)
syncer = ProxyOrderSyncer.new(subscriptions.reload)
t1 = Time.zone.now
syncer.sync!
t2 = Time.zone.now
diff = t2 - t1
times << diff
puts diff.round(2)
expect(ProxyOrder.count).to be 300
subscriptions.update_all(begins_at: start)
end
puts "AVG: #{(times.sum / times.count).round(2)}"
end
end
end
end
end

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
describe Count do
let(:oc1) { create(:simple_order_cycle) }
let(:oc2) { create(:simple_order_cycle) }
let(:subscriptions_count) { Count.new(order_cycles) }
describe "#for" do
context "when the collection has not been set" do
let(:order_cycles) { nil }
it "returns 0" do
expect(subscriptions_count.for(oc1.id)).to eq 0
end
end
context "when the collection has been set" do
let(:order_cycles) { OrderCycle.where(id: [oc1]) }
let!(:po1) { create(:proxy_order, order_cycle: oc1) }
let!(:po2) { create(:proxy_order, order_cycle: oc1) }
let!(:po3) { create(:proxy_order, order_cycle: oc2) }
context "but the requested id is not present in the list of order cycles provided" do
it "returns 0" do
# Note that po3 applies to oc2, but oc2 in not in the collection
expect(subscriptions_count.for(oc2.id)).to eq 0
end
end
context "and the requested id is present in the list of order cycles provided" do
it "returns a count of active proxy orders associated with the requested order cycle" do
expect(subscriptions_count.for(oc1.id)).to eq 2
end
end
end
end
end
end
end

View File

@@ -0,0 +1,171 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
describe Estimator do
describe "estimating prices for subscription line items" do
let!(:subscription) { create(:subscription, with_items: true) }
let!(:sli1) { subscription.subscription_line_items.first }
let!(:sli2) { subscription.subscription_line_items.second }
let!(:sli3) { subscription.subscription_line_items.third }
let(:estimator) { Estimator.new(subscription) }
before do
sli1.update_attributes(price_estimate: 4.0)
sli2.update_attributes(price_estimate: 5.0)
sli3.update_attributes(price_estimate: 6.0)
sli1.variant.update_attributes(price: 1.0)
sli2.variant.update_attributes(price: 2.0)
sli3.variant.update_attributes(price: 3.0)
# Simulating assignment of attrs from params
sli1.assign_attributes(price_estimate: 7.0)
sli2.assign_attributes(price_estimate: 8.0)
sli3.assign_attributes(price_estimate: 9.0)
end
context "when a insufficient information exists to calculate price estimates" do
before do
# This might be because a shop has not been assigned yet, or no
# current or future order cycles exist for the schedule
allow(estimator).to receive(:fee_calculator) { nil }
end
it "resets the price estimates for all items" do
estimator.estimate!
expect(sli1.price_estimate).to eq 4.0
expect(sli2.price_estimate).to eq 5.0
expect(sli3.price_estimate).to eq 6.0
end
end
context "when sufficient information to calculate price estimates exists" do
let(:fee_calculator) { instance_double(OpenFoodNetwork::EnterpriseFeeCalculator) }
before do
allow(estimator).to receive(:fee_calculator) { fee_calculator }
allow(fee_calculator).to receive(:indexed_fees_for).with(sli1.variant) { 1.0 }
allow(fee_calculator).to receive(:indexed_fees_for).with(sli2.variant) { 0.0 }
allow(fee_calculator).to receive(:indexed_fees_for).with(sli3.variant) { 3.0 }
end
context "when no variant overrides apply" do
it "recalculates price_estimates based on variant prices and associated fees" do
estimator.estimate!
expect(sli1.price_estimate).to eq 2.0
expect(sli2.price_estimate).to eq 2.0
expect(sli3.price_estimate).to eq 6.0
end
end
context "when variant overrides apply" do
let!(:override1) {
create(:variant_override, hub: subscription.shop, variant: sli1.variant, price: 1.2)
}
let!(:override2) {
create(:variant_override, hub: subscription.shop, variant: sli2.variant, price: 2.3)
}
it "recalculates price_estimates based on override prices and associated fees" do
estimator.estimate!
expect(sli1.price_estimate).to eq 2.2
expect(sli2.price_estimate).to eq 2.3
expect(sli3.price_estimate).to eq 6.0
end
end
end
end
describe "updating estimates for shipping and payment fees" do
let(:subscription) {
create(:subscription, with_items: true,
payment_method: payment_method,
shipping_method: shipping_method)
}
let!(:sli1) { subscription.subscription_line_items.first }
let!(:sli2) { subscription.subscription_line_items.second }
let!(:sli3) { subscription.subscription_line_items.third }
let(:estimator) { OrderManagement::Subscriptions::Estimator.new(subscription) }
before do
allow(estimator).to receive(:assign_price_estimates)
sli1.update_attributes(price_estimate: 4.0)
sli2.update_attributes(price_estimate: 5.0)
sli3.update_attributes(price_estimate: 6.0)
end
context "using flat rate calculators" do
let(:shipping_method) {
create(:shipping_method,
calculator: Spree::Calculator::FlatRate.new(preferred_amount: 12.34))
}
let(:payment_method) {
create(:payment_method,
calculator: Spree::Calculator::FlatRate.new(preferred_amount: 9.12))
}
it "calculates fees based on the rates provided" do
estimator.estimate!
expect(subscription.shipping_fee_estimate.to_f).to eq 12.34
expect(subscription.payment_fee_estimate.to_f).to eq 9.12
end
end
context "using flat percent item total calculators" do
let(:shipping_method) {
create(:shipping_method,
calculator: Spree::Calculator::FlatPercentItemTotal.new(
preferred_flat_percent: 10
))
}
let(:payment_method) {
create(:payment_method,
calculator: Spree::Calculator::FlatPercentItemTotal.new(
preferred_flat_percent: 20
))
}
it "calculates fees based on the estimated item total and percentage provided" do
estimator.estimate!
expect(subscription.shipping_fee_estimate.to_f).to eq 1.5
expect(subscription.payment_fee_estimate.to_f).to eq 3.0
end
end
context "using flat percent per item calculators" do
let(:shipping_method) {
create(:shipping_method,
calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 5))
}
let(:payment_method) {
create(:payment_method,
calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 10))
}
it "calculates fees based on the estimated item prices and percentage provided" do
estimator.estimate!
expect(subscription.shipping_fee_estimate.to_f).to eq 0.75
expect(subscription.payment_fee_estimate.to_f).to eq 1.5
end
end
context "using per item calculators" do
let(:shipping_method) {
create(:shipping_method,
calculator: Spree::Calculator::PerItem.new(preferred_amount: 1.2))
}
let(:payment_method) {
create(:payment_method,
calculator: Spree::Calculator::PerItem.new(preferred_amount: 0.3))
}
it "calculates fees based on the number of items and rate provided" do
estimator.estimate!
expect(subscription.shipping_fee_estimate.to_f).to eq 3.6
expect(subscription.payment_fee_estimate.to_f).to eq 0.9
end
end
end
end
end
end

View File

@@ -0,0 +1,148 @@
# frozen_string_literal: true
require 'spec_helper'
module OrderManagement
module Subscriptions
describe Form do
describe "creating a new subscription" do
let!(:shop) { create(:distributor_enterprise) }
let!(:customer) { create(:customer, enterprise: shop) }
let!(:product1) { create(:product, supplier: shop) }
let!(:product2) { create(:product, supplier: shop) }
let!(:product3) { create(:product, supplier: shop) }
let!(:variant1) {
create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: [])
}
let!(:variant2) {
create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: [])
}
let!(:variant3) {
create(:variant, product: product2, unit_value: '1000',
price: 2.50, option_values: [], on_hand: 1)
}
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle1) {
create(:simple_order_cycle, coordinator: shop,
orders_open_at: 9.days.ago,
orders_close_at: 2.days.ago)
}
let!(:order_cycle2) {
create(:simple_order_cycle, coordinator: shop,
orders_open_at: 2.days.ago,
orders_close_at: 5.days.from_now)
}
let!(:order_cycle3) {
create(:simple_order_cycle, coordinator: shop,
orders_open_at: 5.days.from_now,
orders_close_at: 12.days.from_now)
}
let!(:order_cycle4) {
create(:simple_order_cycle, coordinator: shop,
orders_open_at: 12.days.from_now,
orders_close_at: 19.days.from_now)
}
let!(:outgoing_exchange1) {
order_cycle1.exchanges.create(sender: shop,
receiver: shop,
variants: [variant1, variant2, variant3],
enterprise_fees: [enterprise_fee])
}
let!(:outgoing_exchange2) {
order_cycle2.exchanges.create(sender: shop,
receiver: shop,
variants: [variant1, variant2, variant3],
enterprise_fees: [enterprise_fee])
}
let!(:outgoing_exchange3) {
order_cycle3.exchanges.create(sender: shop,
receiver: shop,
variants: [variant1, variant3],
enterprise_fees: [])
}
let!(:outgoing_exchange4) {
order_cycle4.exchanges.create(sender: shop,
receiver: shop,
variants: [variant1, variant2, variant3],
enterprise_fees: [enterprise_fee])
}
let!(:schedule) {
create(:schedule, order_cycles: [order_cycle1, order_cycle2, order_cycle3, order_cycle4])
}
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
let!(:address) { create(:address) }
let(:subscription) { Subscription.new }
let!(:params) {
{
shop_id: shop.id,
customer_id: customer.id,
schedule_id: schedule.id,
bill_address_attributes: address.clone.attributes,
ship_address_attributes: address.clone.attributes,
payment_method_id: payment_method.id,
shipping_method_id: shipping_method.id,
begins_at: 4.days.ago,
ends_at: 14.days.from_now,
subscription_line_items_attributes: [
{ variant_id: variant1.id, quantity: 1, price_estimate: 7.0 },
{ variant_id: variant2.id, quantity: 2, price_estimate: 8.0 },
{ variant_id: variant3.id, quantity: 3, price_estimate: 9.0 }
]
}
}
let(:form) { OrderManagement::Subscriptions::Form.new(subscription, params) }
it "creates orders for each order cycle in the schedule" do
expect(form.save).to be true
expect(subscription.proxy_orders.count).to be 2
expect(subscription.subscription_line_items.count).to be 3
expect(subscription.subscription_line_items[0].price_estimate).to eq 13.75
expect(subscription.subscription_line_items[1].price_estimate).to eq 7.75
expect(subscription.subscription_line_items[2].price_estimate).to eq 4.25
# This order cycle has already closed, so no order is initialized
proxy_order1 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle1.id)
expect(proxy_order1).to be nil
# Currently open order cycle, closing after begins_at and before ends_at
proxy_order2 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle2.id)
expect(proxy_order2).to be_a ProxyOrder
order2 = proxy_order2.initialise_order!
expect(order2.line_items.count).to eq 3
expect(order2.line_items.find_by_variant_id(variant3.id).quantity).to be 3
expect(order2.shipments.count).to eq 1
expect(order2.shipments.first.shipping_method).to eq shipping_method
expect(order2.payments.count).to eq 1
expect(order2.payments.first.payment_method).to eq payment_method
expect(order2.payments.first.state).to eq 'checkout'
expect(order2.total).to eq 42
expect(order2.completed?).to be false
# Future order cycle, closing after begins_at and before ends_at
# Adds line items for variants that aren't yet available from the order cycle
proxy_order3 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle3.id)
expect(proxy_order3).to be_a ProxyOrder
order3 = proxy_order3.initialise_order!
expect(order3).to be_a Spree::Order
expect(order3.line_items.count).to eq 3
expect(order2.line_items.find_by_variant_id(variant3.id).quantity).to be 3
expect(order3.shipments.count).to eq 1
expect(order3.shipments.first.shipping_method).to eq shipping_method
expect(order3.payments.count).to eq 1
expect(order3.payments.first.payment_method).to eq payment_method
expect(order3.payments.first.state).to eq 'checkout'
expect(order3.total).to eq 31.50
expect(order3.completed?).to be false
# Future order cycle closing after ends_at
proxy_order4 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle4.id)
expect(proxy_order4).to be nil
end
end
end
end
end

View File

@@ -0,0 +1,231 @@
# frozen_string_literal: true
require 'spec_helper'
module OrderManagement
module Subscriptions
describe PaymentSetup do
let(:order) { create(:order) }
let(:payment_setup) { OrderManagement::Subscriptions::PaymentSetup.new(order) }
describe "#payment" do
context "when only one payment exists on the order" do
let!(:payment) { create(:payment, order: order) }
context "where the payment is pending" do
it { expect(payment_setup.__send__(:payment)).to eq payment }
end
context "where the payment is failed" do
before { payment.update_attribute(:state, 'failed') }
it { expect(payment_setup.__send__(:payment)).to be nil }
end
end
context "when more that one payment exists on the order" do
let!(:payment1) { create(:payment, order: order) }
let!(:payment2) { create(:payment, order: order) }
context "where more than one payment is pending" do
it { expect([payment1, payment2]).to include payment_setup.__send__(:payment) }
end
context "where only one payment is pending" do
before { payment1.update_attribute(:state, 'failed') }
it { expect(payment_setup.__send__(:payment)).to eq payment2 }
end
context "where no payments are pending" do
before do
payment1.update_attribute(:state, 'failed')
payment2.update_attribute(:state, 'failed')
end
it { expect(payment_setup.__send__(:payment)).to be nil }
end
end
end
describe "#call!" do
let!(:payment){ create(:payment, amount: 10) }
context "when no pending payments are present" do
let(:payment_method) { create(:payment_method) }
let(:subscription) { double(:subscription, payment_method_id: payment_method.id) }
before do
allow(order).to receive(:pending_payments).once { [] }
allow(order).to receive(:outstanding_balance) { 5 }
allow(order).to receive(:subscription) { subscription }
end
it "creates a new payment on the order" do
expect{ payment_setup.call! }.to change(Spree::Payment, :count).by(1)
expect(order.payments.first.amount).to eq 5
end
end
context "when a payment is present" do
before { allow(order).to receive(:pending_payments).once { [payment] } }
context "when a credit card is not required" do
before do
allow(payment_setup).to receive(:card_required?) { false }
expect(payment_setup).to_not receive(:card_available?)
expect(payment_setup).to_not receive(:ensure_credit_card)
end
context "when the payment total doesn't match the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
end
end
end
context "when a credit card is required" do
before do
expect(payment_setup).to receive(:card_required?) { true }
end
context "and the payment source is not a credit card" do
before { expect(payment_setup).to receive(:card_set?) { false } }
context "and no default credit card has been set by the customer" do
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: nil) }
end
it "adds an error to the order and does not update the payment" do
expect(payment).to_not receive(:update_attributes)
expect{ payment_setup.call! }.to change(order.errors[:base], :count).from(0).to(1)
end
end
context "and the customer has not authorised the shop to charge to credit cards" do
before do
allow(order).to receive(:user) {
instance_double(Spree::User, default_card: create(:credit_card))
}
allow(order).to receive(:customer) {
instance_double(Customer, allow_charges?: false)
}
end
it "adds an error to the order and does not update the payment" do
expect(payment).to_not receive(:update_attributes)
expect{ payment_setup.call! }.to change(order.errors[:base], :count).from(0).to(1)
end
end
context "and an authorised default credit card is available to charge" do
before do
allow(order).to receive(:user) {
instance_double(Spree::User, default_card: create(:credit_card))
}
allow(order).to receive(:customer) {
instance_double(Customer, allow_charges?: true)
}
end
context "when the payment total doesn't match the order's outstanding balance" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
end
end
end
end
context "and the payment source is already a credit card" do
before { expect(payment_setup).to receive(:card_set?) { true } }
context "when the payment total doesn't match the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 5 } }
it "updates the payment total to reflect the outstanding balance" do
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
end
end
context "when the payment total matches the outstanding balance on the order" do
before { allow(order).to receive(:outstanding_balance) { 10 } }
it "does nothing" do
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
end
end
end
end
end
end
describe "#ensure_credit_card" do
let!(:payment) { create(:payment, source: nil) }
before { allow(payment_setup).to receive(:payment) { payment } }
context "when no default credit card is found" do
before do
allow(order).to receive(:user) { instance_double(Spree::User, default_card: nil) }
end
it "returns false and down not update the payment source" do
expect do
expect(payment_setup.__send__(:ensure_credit_card)).to be false
end.to_not change(payment, :source).from(nil)
end
end
context "when a default credit card is found" do
let(:credit_card) { create(:credit_card) }
before do
allow(order).to receive(:user) {
instance_double(Spree::User, default_card: credit_card)
}
end
context "and charge have not been authorised by the customer" do
before do
allow(order).to receive(:customer) {
instance_double(Customer, allow_charges?: false)
}
end
it "returns false and does not update the payment source" do
expect do
expect(payment_setup.__send__(:ensure_credit_card)).to be false
end.to_not change(payment, :source).from(nil)
end
end
context "and charges have been authorised by the customer" do
before do
allow(order).to receive(:customer) { instance_double(Customer, allow_charges?: true) }
end
it "returns true and stores the credit card as the payment source" do
expect do
expect(payment_setup.__send__(:ensure_credit_card)).to be true
end.to change(payment, :source_id).from(nil).to(credit_card.id)
end
end
end
end
end
end
end

View File

@@ -0,0 +1,437 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
describe ProxyOrderSyncer do
describe "initialization" do
let!(:subscription) { create(:subscription) }
it "raises an error when initialized with an object
that is not a Subscription or an ActiveRecord::Relation" do
expect{ ProxyOrderSyncer.new(subscription) }.to_not raise_error
expect{ ProxyOrderSyncer.new(Subscription.where(id: subscription.id)) }.to_not raise_error
expect{ ProxyOrderSyncer.new("something") }.to raise_error RuntimeError
end
end
describe "#sync!" do
let(:now) { Time.zone.now }
let(:schedule) { create(:schedule) }
let(:closed_oc) { # Closed
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now - 1.minute,
orders_close_at: now)
}
let(:open_oc_closes_before_begins_at_oc) { # Open, but closes before begins at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now - 1.minute,
orders_close_at: now + 59.seconds)
}
let(:open_oc) { # Open & closes between begins at and ends at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now - 1.minute,
orders_close_at: now + 90.seconds)
}
let(:upcoming_closes_before_begins_at_oc) { # Upcoming, but closes before begins at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now + 30.seconds,
orders_close_at: now + 59.seconds)
}
let(:upcoming_closes_on_begins_at_oc) { # Upcoming & closes on begins at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now + 30.seconds,
orders_close_at: now + 1.minute)
}
let(:upcoming_closes_on_ends_at_oc) { # Upcoming & closes on ends at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now + 30.seconds,
orders_close_at: now + 2.minutes)
}
let(:upcoming_closes_after_ends_at_oc) { # Upcoming & closes after ends at
create(:simple_order_cycle, schedules: [schedule],
orders_open_at: now + 30.seconds,
orders_close_at: now + 121.seconds)
}
let(:subscription) {
build(:subscription, schedule: schedule,
begins_at: now + 1.minute,
ends_at: now + 2.minutes)
}
let(:proxy_orders) { subscription.reload.proxy_orders }
let(:order_cycles) { proxy_orders.map(&:order_cycle) }
let(:syncer) { ProxyOrderSyncer.new(subscription) }
context "when the subscription is not persisted" do
before do
oc # Ensure oc is created before we attempt to sync
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
end
context "and the schedule includes a closed oc (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule has an open OC that closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes upcoming oc that closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "creates a new proxy order for that oc" do
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
end
context "when the subscription is persisted" do
before { expect(subscription.save!).to be true }
context "when a proxy order exists" do
let!(:proxy_order) { create(:proxy_order, subscription: subscription, order_cycle: oc) }
context "for an oc included in the relevant schedule" do
context "and the proxy order has already been placed" do
before { proxy_order.update_attributes(placed_at: 5.minutes.ago) }
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
end
context "and the proxy order has not already been placed" do
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
end
end
context "for an oc not included in the relevant schedule" do
let!(:proxy_order) {
create(:proxy_order, subscription: subscription, order_cycle: open_oc)
}
before do
open_oc.schedule_ids = []
expect(open_oc.save!).to be true
end
context "and the proxy order has already been placed" do
before { proxy_order.update_attributes(placed_at: 5.minutes.ago) }
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "keeps the proxy order" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
expect(proxy_orders).to include proxy_order
end
end
end
context "and the proxy order has not already been placed" do
# This shouldn't really happen, but it is possible
context "the oc is closed (ie. closed before opens_at)" do
let(:oc) { closed_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
# This shouldn't really happen, but it is possible
context "and the oc is open and closes between begins_at and ends_at" do
let(:oc) { open_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes before begins_at" do
let(:oc) { upcoming_closes_before_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on begins_at" do
let(:oc) { upcoming_closes_on_begins_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes on ends_at" do
let(:oc) { upcoming_closes_on_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
context "and the oc is upcoming and closes after ends_at" do
let(:oc) { upcoming_closes_after_ends_at_oc }
it "removes the proxy order" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
expect(proxy_orders).to_not include proxy_order
end
end
end
end
end
context "when a proxy order does not exist" do
context "and the schedule includes a closed oc (ie. closed before opens_at)" do
let!(:oc) { closed_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes an open oc that closes before begins_at" do
let(:oc) { open_oc_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule has an open oc that closes between begins_at and ends_at" do
let!(:oc) { open_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes before begins_at" do
let!(:oc) { upcoming_closes_before_begins_at_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
context "and the schedule includes upcoming oc that closes on begins_at" do
let!(:oc) { upcoming_closes_on_begins_at_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes on ends_at" do
let!(:oc) { upcoming_closes_on_ends_at_oc }
it "creates a new proxy order for that oc" do
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
expect(order_cycles).to include oc
end
end
context "and the schedule includes upcoming oc that closes after ends_at" do
let!(:oc) { upcoming_closes_after_ends_at_oc }
it "does not create a new proxy order for that oc" do
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
expect(order_cycles).to_not include oc
end
end
end
end
end
end
end
end

View File

@@ -0,0 +1,132 @@
# frozen_string_literal: true
require 'spec_helper'
module OrderManagement
module Subscriptions
describe Summarizer do
let(:order) { create(:order) }
let(:summarizer) { OrderManagement::Subscriptions::Summarizer.new }
before { allow(Rails.logger).to receive(:info) }
describe "#summary_for" do
let(:order) { double(:order, distributor_id: 123) }
context "when a summary for the order's distributor doesn't already exist" do
it "initializes a new summary object, and returns it" do
expect(summarizer.instance_variable_get(:@summaries).count).to be 0
summary = summarizer.__send__(:summary_for, order)
expect(summary.shop_id).to be 123
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
end
end
context "when a summary for the order's distributor already exists" do
let(:summary) { double(:summary) }
before do
summarizer.instance_variable_set(:@summaries, 123 => summary)
end
it "returns the existing summary object" do
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
expect(summarizer.__send__(:summary_for, order)).to eq summary
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
end
end
end
describe "recording events" do
let(:order) { double(:order) }
let(:summary) { double(:summary) }
before { allow(summarizer).to receive(:summary_for).with(order) { summary } }
describe "#record_order" do
it "requests a summary for the order and calls #record_order on it" do
expect(summary).to receive(:record_order).with(order).once
summarizer.record_order(order)
end
end
describe "#record_success" do
it "requests a summary for the order and calls #record_success on it" do
expect(summary).to receive(:record_success).with(order).once
summarizer.record_success(order)
end
end
describe "#record_issue" do
it "requests a summary for the order and calls #record_issue on it" do
expect(order).to receive(:id)
expect(summary).to receive(:record_issue).with(:type, order, "message").once
summarizer.record_issue(:type, order, "message")
end
end
describe "#record_and_log_error" do
before do
allow(order).to receive(:number) { "123" }
end
context "when errors exist on the order" do
before do
allow(order).to receive(:errors) {
double(:errors, any?: true, full_messages: ["Some error"])
}
end
it "sends error info to rails logger and calls #record_issue with an error message" do
expect(summarizer).to receive(:record_issue).with(:processing,
order, "Errors: Some error")
summarizer.record_and_log_error(:processing, order)
end
end
context "when no errors exist on the order" do
before do
allow(order).to receive(:errors) { double(:errors, any?: false) }
end
it "falls back to calling record_issue" do
expect(summarizer).to receive(:record_issue).with(:processing, order)
summarizer.record_and_log_error(:processing, order)
end
end
end
end
describe "#send_placement_summary_emails" do
let(:summary1) { double(:summary) }
let(:summary2) { double(:summary) }
let(:summaries) { { 1 => summary1, 2 => summary2 } }
let(:mail_mock) { double(:mail, deliver: true) }
before do
summarizer.instance_variable_set(:@summaries, summaries)
end
it "sends a placement summary email for each summary" do
expect(SubscriptionMailer).to receive(:placement_summary_email).twice { mail_mock }
summarizer.send_placement_summary_emails
end
end
describe "#send_confirmation_summary_emails" do
let(:summary1) { double(:summary) }
let(:summary2) { double(:summary) }
let(:summaries) { { 1 => summary1, 2 => summary2 } }
let(:mail_mock) { double(:mail, deliver: true) }
before do
summarizer.instance_variable_set(:@summaries, summaries)
end
it "sends a placement summary email for each summary" do
expect(SubscriptionMailer).to receive(:confirmation_summary_email).twice { mail_mock }
summarizer.send_confirmation_summary_emails
end
end
end
end
end

View File

@@ -0,0 +1,127 @@
# frozen_string_literal: true
module OrderManagement
module Subscriptions
describe Summary do
let(:summary) { OrderManagement::Subscriptions::Summary.new(123) }
describe "#initialize" do
it "initializes instance variables: shop_id, order_count, success_count and issues" do
expect(summary.shop_id).to be 123
expect(summary.order_count).to be 0
expect(summary.success_count).to be 0
expect(summary.issues).to be_a Hash
end
end
describe "#record_order" do
let(:order) { double(:order, id: 37) }
it "adds the order id to the order_ids array" do
summary.record_order(order)
expect(summary.instance_variable_get(:@order_ids)).to eq [order.id]
end
end
describe "#record_success" do
let(:order) { double(:order, id: 37) }
it "adds the order id to the success_ids array" do
summary.record_success(order)
expect(summary.instance_variable_get(:@success_ids)).to eq [order.id]
end
end
describe "#record_issue" do
let(:order) { double(:order, id: 1) }
context "when no issues of the same type have been recorded yet" do
it "adds a new type to the issues hash, and stores a new issue against it" do
summary.record_issue(:some_type, order, "message")
expect(summary.issues.keys).to include :some_type
expect(summary.issues[:some_type][order.id]).to eq "message"
end
end
context "when an issue of the same type has already been recorded" do
let(:existing_issue) { double(:existing_issue) }
before { summary.issues[:some_type] = [existing_issue] }
it "stores a new issue against the existing type" do
summary.record_issue(:some_type, order, "message")
expect(summary.issues[:some_type]).to include existing_issue
expect(summary.issues[:some_type][order.id]).to eq "message"
end
end
end
describe "#order_count" do
let(:order_ids) { [1, 2, 3, 4, 5, 6, 7] }
it "counts the number of items in the order_ids instance_variable" do
summary.instance_variable_set(:@order_ids, order_ids)
expect(summary.order_count).to be 7
end
end
describe "#success_count" do
let(:success_ids) { [1, 2, 3, 4, 5, 6, 7] }
it "counts the number of items in the success_ids instance_variable" do
summary.instance_variable_set(:@success_ids, success_ids)
expect(summary.success_count).to be 7
end
end
describe "#issue_count" do
let(:order_ids) { [1, 3, 5, 7, 9] }
let(:success_ids) { [1, 2, 3, 4, 5] }
it "counts the number of items in order_ids that are not in success_ids" do
summary.instance_variable_set(:@order_ids, order_ids)
summary.instance_variable_set(:@success_ids, success_ids)
expect(summary.issue_count).to be 2 # 7 & 9
end
end
describe "#orders_affected_by" do
let(:order1) { create(:order) }
let(:order2) { create(:order) }
before do
allow(summary).to receive(:unrecorded_ids) { [order1.id] }
allow(summary).to receive(:issues) { { failure: { order2.id => "A message" } } }
end
context "when the issue type is :other" do
let(:orders) { summary.orders_affected_by(:other) }
it "returns orders specified by unrecorded_ids" do
expect(orders).to include order1
expect(orders).to_not include order2
end
end
context "when the issue type is :other" do
let(:orders) { summary.orders_affected_by(:failure) }
it "returns orders specified by the relevant issue hash" do
expect(orders).to include order2
expect(orders).to_not include order1
end
end
end
describe "#unrecorded_ids" do
let(:issues) { { type: { 7 => "message", 8 => "message" } } }
before do
summary.instance_variable_set(:@order_ids, [1, 3, 5, 7, 9])
summary.instance_variable_set(:@success_ids, [1, 2, 3, 4, 5])
summary.instance_variable_set(:@issues, issues)
end
it "returns order_ids that are not marked as an issue or a success" do
expect(summary.unrecorded_ids).to eq [9]
end
end
end
end
end

View File

@@ -0,0 +1,524 @@
# frozen_string_literal: true
require "spec_helper"
module OrderManagement
module Subscriptions
describe Validator do
let(:owner) { create(:user) }
let(:shop) { create(:enterprise, name: "Shop", owner: owner) }
describe "delegation" do
let(:subscription) { create(:subscription, shop: shop) }
let(:validator) { Validator.new(subscription) }
it "delegates to subscription" do
expect(validator.shop).to eq subscription.shop
expect(validator.customer).to eq subscription.customer
expect(validator.schedule).to eq subscription.schedule
expect(validator.shipping_method).to eq subscription.shipping_method
expect(validator.payment_method).to eq subscription.payment_method
expect(validator.bill_address).to eq subscription.bill_address
expect(validator.ship_address).to eq subscription.ship_address
expect(validator.begins_at).to eq subscription.begins_at
expect(validator.ends_at).to eq subscription.ends_at
end
end
describe "validations" do
let(:subscription_stubs) do
{
shop: shop,
customer: true,
schedule: true,
shipping_method: true,
payment_method: true,
bill_address: true,
ship_address: true,
begins_at: true,
ends_at: true,
}
end
let(:validation_stubs) do
{
shipping_method_allowed?: true,
payment_method_allowed?: true,
payment_method_type_allowed?: true,
ends_at_after_begins_at?: true,
customer_allowed?: true,
schedule_allowed?: true,
credit_card_ok?: true,
subscription_line_items_present?: true,
requested_variants_available?: true
}
end
let(:subscription) { instance_double(Subscription, subscription_stubs) }
let(:validator) { OrderManagement::Subscriptions::Validator.new(subscription) }
def stub_validations(validator, methods)
methods.each do |name, value|
allow(validator).to receive(name) { value }
end
end
describe "shipping method validation" do
let(:subscription) {
instance_double(Subscription, subscription_stubs.except(:shipping_method))
}
before { stub_validations(validator, validation_stubs.except(:shipping_method_allowed?)) }
context "when no shipping method is present" do
before { expect(subscription).to receive(:shipping_method).at_least(:once) { nil } }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:shipping_method]).to_not be_empty
end
end
context "when a shipping method is present" do
let(:shipping_method) { instance_double(Spree::ShippingMethod, distributors: [shop]) }
before {
expect(subscription).to receive(:shipping_method).at_least(:once) { shipping_method }
}
context "and the shipping method is not associated with the shop" do
before { allow(shipping_method).to receive(:distributors) { [double(:enterprise)] } }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:shipping_method]).to_not be_empty
end
end
context "and the shipping method is associated with the shop" do
before { allow(shipping_method).to receive(:distributors) { [shop] } }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:shipping_method]).to be_empty
end
end
end
end
describe "payment method validation" do
let(:subscription) {
instance_double(Subscription, subscription_stubs.except(:payment_method))
}
before { stub_validations(validator, validation_stubs.except(:payment_method_allowed?)) }
context "when no payment method is present" do
before { expect(subscription).to receive(:payment_method).at_least(:once) { nil } }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:payment_method]).to_not be_empty
end
end
context "when a payment method is present" do
let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) }
before {
expect(subscription).to receive(:payment_method).at_least(:once) { payment_method }
}
context "and the payment method is not associated with the shop" do
before { allow(payment_method).to receive(:distributors) { [double(:enterprise)] } }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:payment_method]).to_not be_empty
end
end
context "and the payment method is associated with the shop" do
before { allow(payment_method).to receive(:distributors) { [shop] } }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:payment_method]).to be_empty
end
end
end
end
describe "payment method type validation" do
let(:subscription) {
instance_double(Subscription, subscription_stubs.except(:payment_method))
}
before {
stub_validations(validator, validation_stubs.except(:payment_method_type_allowed?))
}
context "when a payment method is present" do
let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) }
before {
expect(subscription).to receive(:payment_method).at_least(:once) { payment_method }
}
context "and the payment method type is not in the approved list" do
before { allow(payment_method).to receive(:type) { "Blah" } }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:payment_method]).to_not be_empty
end
end
context "and the payment method is in the approved list" do
let(:approved_type) { Subscription::ALLOWED_PAYMENT_METHOD_TYPES.first }
before { allow(payment_method).to receive(:type) { approved_type } }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:payment_method]).to be_empty
end
end
end
end
describe "dates" do
let(:subscription) {
instance_double(Subscription, subscription_stubs.except(:begins_at, :ends_at))
}
before { stub_validations(validator, validation_stubs.except(:ends_at_after_begins_at?)) }
before { expect(subscription).to receive(:begins_at).at_least(:once) { begins_at } }
context "when no begins_at is present" do
let(:begins_at) { nil }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:begins_at]).to_not be_empty
end
end
context "when a start date is present" do
let(:begins_at) { Time.zone.today }
before { expect(subscription).to receive(:ends_at).at_least(:once) { ends_at } }
context "when no ends_at is present" do
let(:ends_at) { nil }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:ends_at]).to be_empty
end
end
context "when ends_at is equal to begins_at" do
let(:ends_at) { Time.zone.today }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:ends_at]).to_not be_empty
end
end
context "when ends_at is before begins_at" do
let(:ends_at) { Time.zone.today - 1.day }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:ends_at]).to_not be_empty
end
end
context "when ends_at is after begins_at" do
let(:ends_at) { Time.zone.today + 1.day }
it "adds an error and returns false" do
expect(validator.valid?).to be true
expect(validator.errors[:ends_at]).to be_empty
end
end
end
end
describe "addresses" do
before { stub_validations(validator, validation_stubs) }
let(:subscription) {
instance_double(Subscription, subscription_stubs.except(:bill_address, :ship_address))
}
before { expect(subscription).to receive(:bill_address).at_least(:once) { bill_address } }
before { expect(subscription).to receive(:ship_address).at_least(:once) { ship_address } }
context "when bill_address and ship_address are not present" do
let(:bill_address) { nil }
let(:ship_address) { nil }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:bill_address]).to_not be_empty
expect(validator.errors[:ship_address]).to_not be_empty
end
end
context "when bill_address and ship_address are present" do
let(:bill_address) { instance_double(Spree::Address) }
let(:ship_address) { instance_double(Spree::Address) }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:bill_address]).to be_empty
expect(validator.errors[:ship_address]).to be_empty
end
end
end
describe "customer" do
let(:subscription) { instance_double(Subscription, subscription_stubs.except(:customer)) }
before { stub_validations(validator, validation_stubs.except(:customer_allowed?)) }
before { expect(subscription).to receive(:customer).at_least(:once) { customer } }
context "when no customer is present" do
let(:customer) { nil }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:customer]).to_not be_empty
end
end
context "when a customer is present" do
let(:customer) { instance_double(Customer) }
context "and the customer is not associated with the shop" do
before { allow(customer).to receive(:enterprise) { double(:enterprise) } }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:customer]).to_not be_empty
end
end
context "and the customer is associated with the shop" do
before { allow(customer).to receive(:enterprise) { shop } }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:customer]).to be_empty
end
end
end
end
describe "schedule" do
let(:subscription) { instance_double(Subscription, subscription_stubs.except(:schedule)) }
before { stub_validations(validator, validation_stubs.except(:schedule_allowed?)) }
before { expect(subscription).to receive(:schedule).at_least(:once) { schedule } }
context "when no schedule is present" do
let(:schedule) { nil }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:schedule]).to_not be_empty
end
end
context "when a schedule is present" do
let(:schedule) { instance_double(Schedule) }
context "and the schedule is not associated with the shop" do
before { allow(schedule).to receive(:coordinators) { [double(:enterprise)] } }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:schedule]).to_not be_empty
end
end
context "and the schedule is associated with the shop" do
before { allow(schedule).to receive(:coordinators) { [shop] } }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:schedule]).to be_empty
end
end
end
end
describe "credit card" do
let(:subscription) {
instance_double(Subscription, subscription_stubs.except(:payment_method))
}
before { stub_validations(validator, validation_stubs.except(:credit_card_ok?)) }
before {
expect(subscription).to receive(:payment_method).at_least(:once) { payment_method }
}
context "when using a Check payment method" do
let(:payment_method) {
instance_double(Spree::PaymentMethod, type: "Spree::PaymentMethod::Check")
}
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:subscription_line_items]).to be_empty
end
end
context "when using the StripeConnect payment gateway" do
let(:payment_method) {
instance_double(Spree::PaymentMethod, type: "Spree::Gateway::StripeConnect")
}
before { expect(subscription).to receive(:customer).at_least(:once) { customer } }
context "when the customer does not allow charges" do
let(:customer) { instance_double(Customer, allow_charges: false) }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:payment_method]).to_not be_empty
end
end
context "when the customer allows charges" do
let(:customer) { instance_double(Customer, allow_charges: true) }
context "and the customer is not associated with a user" do
before { allow(customer).to receive(:user) { nil } }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:payment_method]).to_not be_empty
end
end
context "and the customer is associated with a user" do
before { expect(customer).to receive(:user).once { user } }
context "and the user has no default card set" do
let(:user) { instance_double(Spree::User, default_card: nil) }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:payment_method]).to_not be_empty
end
end
context "and the user has a default card set" do
let(:user) { instance_double(Spree::User, default_card: 'some card') }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:payment_method]).to be_empty
end
end
end
end
end
end
describe "subscription line items" do
let(:subscription) { instance_double(Subscription, subscription_stubs) }
before {
stub_validations(validator, validation_stubs.except(:subscription_line_items_present?))
}
before {
expect(subscription).to receive(:subscription_line_items).at_least(:once) {
subscription_line_items
}
}
context "when no subscription line items exist" do
let(:subscription_line_items) { [] }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:subscription_line_items]).to_not be_empty
end
end
context "when subscription line items exist but all are marked for destruction" do
let(:subscription_line_item1) {
instance_double(SubscriptionLineItem, marked_for_destruction?: true)
}
let(:subscription_line_items) { [subscription_line_item1] }
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:subscription_line_items]).to_not be_empty
end
end
context "when subscription line items exist and some are not marked for destruction" do
let(:subscription_line_item1) {
instance_double(SubscriptionLineItem, marked_for_destruction?: true)
}
let(:subscription_line_item2) {
instance_double(SubscriptionLineItem, marked_for_destruction?: false)
}
let(:subscription_line_items) { [subscription_line_item1, subscription_line_item2] }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:subscription_line_items]).to be_empty
end
end
end
describe "variant availability" do
let(:subscription) { instance_double(Subscription, subscription_stubs) }
before {
stub_validations(validator, validation_stubs.except(:requested_variants_available?))
}
before {
expect(subscription).to receive(:subscription_line_items).at_least(:once) {
subscription_line_items
}
}
context "when no subscription line items exist" do
let(:subscription_line_items) { [] }
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:subscription_line_items]).to be_empty
end
end
context "when subscription line items exist" do
let(:variant1) { instance_double(Spree::Variant, id: 1) }
let(:variant2) { instance_double(Spree::Variant, id: 2) }
let(:subscription_line_item1) {
instance_double(SubscriptionLineItem, variant: variant1)
}
let(:subscription_line_item2) {
instance_double(SubscriptionLineItem, variant: variant2)
}
let(:subscription_line_items) { [subscription_line_item1] }
context "but some variants are unavailable" do
let(:product) { instance_double(Spree::Product, name: "some_name") }
before do
allow(validator).to receive(:available_variant_ids) { [variant2.id] }
allow(variant1).to receive(:product) { product }
allow(variant1).to receive(:full_name) { "some name" }
end
it "adds an error and returns false" do
expect(validator.valid?).to be false
expect(validator.errors[:subscription_line_items]).to_not be_empty
end
end
context "and all requested variants are available" do
before do
allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] }
end
it "returns true" do
expect(validator.valid?).to be true
expect(validator.errors[:subscription_line_items]).to be_empty
end
end
end
end
end
end
end
end

View File

@@ -0,0 +1,165 @@
# frozen_string_literal: true
require "spec_helper"
module OrderManagement
module Subscriptions
describe VariantsList do
describe "variant eligibility for subscription" do
let!(:shop) { create(:distributor_enterprise) }
let!(:producer) { create(:supplier_enterprise) }
let!(:product) { create(:product, supplier: producer) }
let!(:variant) { product.variants.first }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:subscription) { create(:subscription, shop: shop, schedule: schedule) }
let!(:subscription_line_item) do
create(:subscription_line_item, subscription: subscription, variant: variant)
end
let(:current_order_cycle) do
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.ago,
orders_close_at: 1.week.from_now)
end
let(:future_order_cycle) do
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.from_now,
orders_close_at: 2.weeks.from_now)
end
let(:past_order_cycle) do
create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.weeks.ago,
orders_close_at: 1.week.ago)
end
let!(:order_cycle) { current_order_cycle }
context "if the shop is the supplier for the product" do
let!(:producer) { shop }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
context "if the supplier is permitted for the shop" do
let!(:enterprise_relationship) {
create(:enterprise_relationship, child: shop,
parent: product.supplier,
permissions_list: [:add_to_order_cycle])
}
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
context "if the variant is involved in an exchange" do
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
context "if it is an incoming exchange where the shop is the receiver" do
let!(:incoming_exchange) {
order_cycle.exchanges.create(sender: product.supplier,
receiver: shop,
incoming: true, variants: [variant])
}
it "is not eligible" do
expect(described_class.eligible_variants(shop)).to_not include(variant)
end
end
context "if it is an outgoing exchange where the shop is the receiver" do
let!(:outgoing_exchange) {
order_cycle.exchanges.create(sender: product.supplier,
receiver: shop,
incoming: false,
variants: [variant])
}
context "if the order cycle is currently open" do
let!(:order_cycle) { current_order_cycle }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
context "if the order cycle opens in the future" do
let!(:order_cycle) { future_order_cycle }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
context "if the order cycle closed in the past" do
let!(:order_cycle) { past_order_cycle }
it "is eligible" do
expect(described_class.eligible_variants(shop)).to include(variant)
end
end
end
end
context "if the variant is unrelated" do
it "is not eligible" do
expect(described_class.eligible_variants(shop)).to_not include(variant)
end
end
end
describe "checking if variant in open and upcoming order cycles" do
let!(:shop) { create(:enterprise) }
let!(:product) { create(:product) }
let!(:variant) { product.variants.first }
let!(:schedule) { create(:schedule) }
context "if the variant is involved in an exchange" do
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
context "if it is an incoming exchange where the shop is the receiver" do
let!(:incoming_exchange) {
order_cycle.exchanges.create(sender: product.supplier,
receiver: shop,
incoming: true,
variants: [variant])
}
it "is is false" do
expect(described_class).not_to be_in_open_and_upcoming_order_cycles(shop,
schedule,
variant)
end
end
context "if it is an outgoing exchange where the shop is the receiver" do
let!(:outgoing_exchange) {
order_cycle.exchanges.create(sender: product.supplier,
receiver: shop,
incoming: false,
variants: [variant])
}
it "is true" do
expect(described_class).to be_in_open_and_upcoming_order_cycles(shop,
schedule,
variant)
end
end
end
context "if the variant is unrelated" do
it "is false" do
expect(described_class).to_not be_in_open_and_upcoming_order_cycles(shop,
schedule,
variant)
end
end
end
end
end
end

View File

@@ -1,95 +0,0 @@
module OpenFoodNetwork
class ProxyOrderSyncer
attr_reader :subscription
delegate :order_cycles, :proxy_orders, :begins_at, :ends_at, to: :subscription
def initialize(subscriptions)
case subscriptions
when Subscription
@subscription = subscriptions
when ActiveRecord::Relation
@subscriptions = subscriptions.not_ended.not_canceled
else
raise "ProxyOrderSyncer must be initialized with " \
"an instance of Subscription or ActiveRecord::Relation"
end
end
def sync!
return sync_subscriptions! if @subscriptions
return initialise_proxy_orders! unless @subscription.id
sync_subscription!
end
private
def sync_subscriptions!
@subscriptions.each do |subscription|
@subscription = subscription
sync_subscription!
end
end
def initialise_proxy_orders!
uninitialised_order_cycle_ids.each do |order_cycle_id|
Rails.logger.info "Initializing Proxy Order " \
"of subscription #{@subscription.id} in order cycle #{order_cycle_id}"
proxy_orders << ProxyOrder.new(subscription: subscription, order_cycle_id: order_cycle_id)
end
end
def sync_subscription!
Rails.logger.info "Syncing Proxy Orders of subscription #{@subscription.id}"
create_proxy_orders!
remove_orphaned_proxy_orders!
end
def create_proxy_orders!
return unless not_closed_in_range_order_cycles.any?
query = "INSERT INTO proxy_orders (subscription_id, order_cycle_id, updated_at, created_at)"
query << " VALUES #{insert_values}"
query << " ON CONFLICT DO NOTHING"
ActiveRecord::Base.connection.exec_query(query)
end
def uninitialised_order_cycle_ids
not_closed_in_range_order_cycles.pluck(:id) - proxy_orders.map(&:order_cycle_id)
end
def remove_orphaned_proxy_orders!
orphaned_proxy_orders.scoped.delete_all
end
# Remove Proxy Orders that have not been placed yet
# and are in Order Cycles that are out of range
def orphaned_proxy_orders
orphaned = proxy_orders.where(placed_at: nil)
order_cycle_ids = in_range_order_cycles.pluck(:id)
return orphaned unless order_cycle_ids.any?
orphaned.where('order_cycle_id NOT IN (?)', order_cycle_ids)
end
def insert_values
now = Time.now.utc.iso8601
not_closed_in_range_order_cycles
.map{ |oc| "(#{subscription.id},#{oc.id},'#{now}','#{now}')" }
.join(",")
end
def not_closed_in_range_order_cycles
in_range_order_cycles.merge(OrderCycle.not_closed)
end
def in_range_order_cycles
order_cycles.where("orders_close_at >= ? AND orders_close_at <= ?",
begins_at,
ends_at || 100.years.from_now)
end
end
end

View File

@@ -61,7 +61,7 @@ module OpenFoodNetwork
end
def scope_to_eligible_for_subscriptions_in_distributor
eligible_variants_scope = SubscriptionVariantsService.eligible_variants(distributor)
eligible_variants_scope = OrderManagement::Subscriptions::VariantsList.eligible_variants(distributor)
@variants = @variants.merge(eligible_variants_scope)
scope_variants_to_distributor(@variants, distributor)
end

Some files were not shown because too many files have changed in this diff Show More