Compare commits

...

176 Commits

Author SHA1 Message Date
Gaetan Craig-Riou
9816332601 Update all locales with the latest Transifex translations 2026-03-10 22:15:29 +11:00
Gaetan Craig-Riou
4810b02233 Merge pull request #13702 from marincarroll/improve-accessibility-of-admin-pagination
Improve accessibility of admin pagination
2026-03-10 22:12:57 +11:00
Gaetan Craig-Riou
f8716f8005 Merge pull request #13963 from rioug/13855-payment-with-credit
Payment with credit
2026-03-10 22:11:40 +11:00
Gaetan Craig-Riou
5cf213f22a Fix failing spec 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
251a1acffc Clean up enterprise controller 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
d5dec05ab1 Remove payment method ApiCustomerCredit 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
7790259c27 Add transaction origin for internal credit payments 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
6a99d2a3c8 Add transaction origin in descriptiopn 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
e7a2b7ea48 Remove payment method from customer account transaction 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
bd0dcd99f3 Fix failing spec
I wrongly fixed it due to my local database having rogue data
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
bc23423521 Remove the link between enterprise and internal payment method
Enterprise have access to the internal payment method by default.
The access is handled at the application level so we don't have to
manage database links.
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
a4ca56c7a5 Refactored internal payment method
We now check on known payment method type, instead of using the internal
collumn.
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
9f7655852d Provide helper method to get internal payment method 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
ec106a8f83 Add new payment method ApiCustomerCredit
It was previously modelled by a "Check" payment method
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
2e7237197a Refactor customer credit payment method
Set fixed name and description
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
25c579c478 Per review, small code fixes 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
66b820869c Add missing reference to spree_payments 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
ffc819ea76 Linting migration 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
ed3f928783 Fix diplaying customer transactions when no transaction 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
63a9601812 Add scroll bar to modal component
When content is too big, the bottom is displayed off screen
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
3b068b7125 Add created_by to customer account transactions
It allows tracking of who credited the customer via :
- customer account transaction API
- order payment screen
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
597c0590ed Add the ability to show ID on backoffice customer screen
The customer ID is needed for the customer account transaction api
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
b9b91620ef Make sure to link credit payment method on create and update 2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
c67d47a773 Check if internal payment can be voided
Add extra security, we don't want to void a credit payment that is not
completed, otherwise we would be refunding credit that was not used.
A credit payment should not be in a non completed state, but you never
know.
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
85e0da8aeb Improve concurrency spec
Add checks to see if breakpoint is actually reach and if we have a race
condition.
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
34c91613f7 Customer account transaction, simplify balance calculation
Lock the customer to ensure the balance calculation is correct. Much
simpler than locking the first transaction.
2026-03-10 16:07:43 +11:00
Gaetan Craig-Riou
219e3ca9c8 Fix typo 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
628810eb33 Fix spec to work with the internal payment method 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
d95aac333b Add internal to payment method
It's used to hide the payment method used for paying with credit. These
payment method are for internal use only.
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
3e2e7f1799 Add button to credit customer when order is credit owed state
It will add a negative payment, matching the amount credited in the
customer_account_transaction
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
ee13a3abaf Add Orders::CustomerCreditService.refund
It will be used to credit the customer any fund from an order in
credit_owed state
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
d5bd8fa086 Add ability to "void" a customer credit payment
Voiding the payment will refund the credit used to the customer.
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
5e4cd4d51d Remove unused helper 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
c0823d24c2 Fix deprecation warning 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
41e4fd79a3 Move logic to apply customer credit to it's on service
It's now called as a before transition callback when the order move to
payment. We need to apply the credit at this point to account for
order fees, ie: shippment fees.
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
a60afd10e4 Display credit used on the order confirmation page 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
b42b10fcd1 Use the order outstanding balance to create payment
When a customer credit is applied to an order, the balance due is the
order outstanding balance and not the order total.
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
c8dbf4c6f0 Add error handling around payment with credit
For now we log error but don't report it to the user, so they can
proceed through the checkout.
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
f5a3093e41 Automatically use credit at checkout when available
This only cover the ideal scenario, error handling will be added later
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
5a376c9106 Add customer_credit scope 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
152fd15bd0 Use display_name and display_description for payment method 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
5bdb6e6d69 Clean up spec 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
cb6b1f2dd0 Link producer enterprise to credit payment method 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
130401263a Add CreditPaymentMethod::Linker
It links the given enterprise to credit related payment method. It
will create the methods if missing.
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
29a8a6641c Add ability to use payment with credit
Currently it works like any other payment method you can select on
checkout. It will eventually be added automatically to the order, when a
customer has credit available.
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
7ab33d86f1 Add Customer credit payment method migration 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
28241756aa Add Customer Credit payment method
It doesn't do anything yet
2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
fec5516fce Backoffice customer, Add pop up to list customer payments 2026-03-10 16:07:42 +11:00
Gaetan Craig-Riou
6aa4bf7a33 Add available credit on the admin customer page 2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
c58a65a52b Add a tab to list customer payment on the account page 2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
e21fadd124 Add CustomerAccountTransactions::DataLoaderService
It's used to load customer transactions related to a user and a specific
enterprise
2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
1b468522e6 Add concurrency spec
Make sure two transactions cannot be processed at the same time
2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
be7be9bbc6 Add api endpoing to create customer transactions
Plus specs and documentation
2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
6915836a14 Add balance to customer account transaction
It stores the running balance for the customer account, so we don't have
to calulate it on the fly.
2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
e10fd0b020 Add api payment method
It will be used to credit customer via the customer account transaction
API
2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
45786780a8 Code formating 2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
7d400e9860 Add CustomerAccountTransaction model
It's used to store payment against a customer, to model customer credit
2026-03-10 16:07:41 +11:00
Gaetan Craig-Riou
b50f299381 Merge pull request #14008 from rioug/14004-link-correct-customer-to-order
14004 link correct customer to order
2026-03-10 16:04:45 +11:00
Gaetan Craig-Riou
d706b3a6c2 Per review, remove customer when no existing customer 2026-03-10 15:20:56 +11:00
Gaetan Craig-Riou
153b94f18e Merge pull request #14006 from openfoodfoundation/dependabot/bundler/turbo-rails-2.0.23
Bump turbo-rails from 2.0.20 to 2.0.23
2026-03-10 14:55:27 +11:00
Gaetan Craig-Riou
71daccb49a Merge pull request #14005 from openfoodfoundation/dependabot/npm_and_yarn/webpack-5.105.3
Bump webpack from 5.105.2 to 5.105.3
2026-03-10 14:53:47 +11:00
Gaetan Craig-Riou
bca5ee226d Add spec to test the correct customer is linked 2026-03-10 14:16:01 +11:00
Gaetan Craig-Riou
fe00d1f813 reset_other! now reset the customer when present 2026-03-10 14:16:01 +11:00
Gaetan Craig-Riou
57a69f7fa3 Improve spec for reset_other!
Covers resetting the user
2026-03-10 14:15:54 +11:00
dependabot[bot]
f4d972fc5e Bump turbo-rails from 2.0.20 to 2.0.23
Bumps [turbo-rails](https://github.com/hotwired/turbo-rails) from 2.0.20 to 2.0.23.
- [Release notes](https://github.com/hotwired/turbo-rails/releases)
- [Commits](https://github.com/hotwired/turbo-rails/compare/v2.0.20...v2.0.23)

---
updated-dependencies:
- dependency-name: turbo-rails
  dependency-version: 2.0.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 09:33:08 +00:00
dependabot[bot]
6db3b44e92 Bump webpack from 5.105.2 to 5.105.3
Bumps [webpack](https://github.com/webpack/webpack) from 5.105.2 to 5.105.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack/compare/v5.105.2...v5.105.3)

---
updated-dependencies:
- dependency-name: webpack
  dependency-version: 5.105.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 09:32:45 +00:00
Maikel
f7f7a5738a Merge pull request #13895 from cillian/remove-angular-from-enterprise-settings-users
Remove Angular from Enterprise > Settings > Users section
2026-03-06 14:05:13 +11:00
Gaetan Craig-Riou
da912f21b3 Merge pull request #14003 from openfoodfoundation/dependabot/npm_and_yarn/dompurify-3.3.2
Bump dompurify from 3.3.1 to 3.3.2
2026-03-06 11:29:51 +11:00
dependabot[bot]
e2146eb1a3 Bump dompurify from 3.3.1 to 3.3.2
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.1...3.3.2)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-05 23:10:00 +00:00
Gaetan Craig-Riou
8e19bfca3c Merge pull request #13991 from openfoodfoundation/dependabot/npm_and_yarn/immutable-5.1.5
Bump immutable from 5.1.4 to 5.1.5
2026-03-06 09:59:29 +11:00
Gaetan Craig-Riou
255836b4d2 Merge pull request #13990 from openfoodfoundation/dependabot/npm_and_yarn/svgo-4.0.1
Bump svgo from 4.0.0 to 4.0.1
2026-03-06 09:54:52 +11:00
Gaetan Craig-Riou
fcd7b457e6 Merge pull request #13989 from openfoodfoundation/dependabot/bundler/vcr-6.4.0
Bump vcr from 6.3.1 to 6.4.0
2026-03-06 09:53:05 +11:00
Gaetan Craig-Riou
eb1daf425a Merge pull request #13988 from openfoodfoundation/dependabot/npm_and_yarn/hotkeys-js-4.0.2
Bump hotkeys-js from 4.0.1 to 4.0.2
2026-03-06 09:51:44 +11:00
Gaetan Craig-Riou
06bc6c276a Merge pull request #13987 from mkllnk/spring-watch
Add gem for spring to listen to filesystem changes efficiently
2026-03-06 09:50:32 +11:00
Gaetan Craig-Riou
6854a53bd1 Merge pull request #13785 from pacodelaluna/repair-rounding-issue-on-totals-in-reports
Repair rounding issue on totals in reports
2026-03-06 09:34:39 +11:00
Rachel Arnould
57186a74a8 Merge pull request #13958 from arunguleria/13864-remove-variants-dead-features
13864-Remove dead feature to re-arrange variants
2026-03-05 12:44:05 +01:00
dependabot[bot]
3575952d4b Bump immutable from 5.1.4 to 5.1.5
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.4 to 5.1.5.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.4...v5.1.5)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 23:24:58 +00:00
dependabot[bot]
11203da5ca Bump svgo from 4.0.0 to 4.0.1
Bumps [svgo](https://github.com/svg/svgo) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 4.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 23:08:16 +00:00
dependabot[bot]
86091a5a79 Bump vcr from 6.3.1 to 6.4.0
Bumps [vcr](https://github.com/vcr/vcr) from 6.3.1 to 6.4.0.
- [Release notes](https://github.com/vcr/vcr/releases)
- [Changelog](https://github.com/vcr/vcr/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vcr/vcr/compare/v6.3.1...v6.4.0)

---
updated-dependencies:
- dependency-name: vcr
  dependency-version: 6.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 09:35:06 +00:00
dependabot[bot]
f4328f1d18 Bump hotkeys-js from 4.0.1 to 4.0.2
Bumps [hotkeys-js](https://github.com/jaywcjlove/hotkeys-js) from 4.0.1 to 4.0.2.
- [Release notes](https://github.com/jaywcjlove/hotkeys-js/releases)
- [Commits](https://github.com/jaywcjlove/hotkeys-js/compare/v4.0.1...v4.0.2)

---
updated-dependencies:
- dependency-name: hotkeys-js
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 09:34:51 +00:00
Marin @Home
2334a695a8 adds back CSS hiding disabled prev/next for similar pagination components 2026-03-04 14:43:43 +11:00
Marin @Home
c9418c52e4 linting 2026-03-04 14:43:43 +11:00
Marin @Home
ce1e38f97b Fixes 'orders' test failure 2026-03-04 14:43:43 +11:00
Marin @Home
e16595eacb Adds empty href; prev/next button labels 2026-03-04 14:43:43 +11:00
Marin @Home
510fd4867b Removes disabled/hidden prev/next buttons 2026-03-04 14:43:43 +11:00
Marin @Home
53d6886f20 <ul> should only wrap pagelist 2026-03-04 14:43:43 +11:00
Marin @Home
dad9014a60 Adds ARIA labels to page button/links 2026-03-04 14:43:43 +11:00
Marin @Home
abe9032910 Adds rel "next" and "prev" to pagination 2026-03-04 14:43:43 +11:00
Marin @Home
272cf9ae87 Adds ARIA current "page" attribute 2026-03-04 14:43:43 +11:00
Marin @Home
1d5bc14f2f Adds ARIA label to pagination with English translation 2026-03-04 14:43:43 +11:00
Marin @Home
0332049551 Updates CSS after pagination markup change 2026-03-04 14:43:43 +11:00
Marin @Home
0ffd4dcc35 Pagination should use ul > li pattern 2026-03-04 14:43:42 +11:00
Marin @Home
e899d1b7fd Button element should be <a> 2026-03-04 14:43:42 +11:00
Marin @Home
ed331dc104 Pagination wrapper should be <nav> 2026-03-04 14:43:42 +11:00
Maikel Linke
1bec028a09 Add gem for spring to listen to filesystem changes efficiently
I found that each spring process was using around 3% CPU in the
background just scanning for file changes. By default, spring polls the
file system every 0.2 seconds.

With the added gem, I can't see any CPU use of spring in the background.
2026-03-04 14:43:18 +11:00
David Cook
59547ba9e4 Merge pull request #13964 from mkllnk/flaky-spec
Stabilise actions_spec
2026-03-04 13:06:08 +11:00
Maikel
9fb8bb15e8 Merge pull request #13951 from zilton7/fix/spree-credit-card-brand-deprecation
Fix Spree::CreditCard#brand= deprecation for Rails 7.2 compatibility
2026-03-04 11:01:10 +11:00
Maikel
8aa89c0bf7 Merge pull request #13669 from pacodelaluna/repair-proxy-order-to-support-order-cycle-without-closing-time
Repair ProxyOrder to support order cycle without closing time
2026-03-04 10:59:21 +11:00
Cillian O'Ruanaidh
447d80c960 Fix spec due to switch to email type field 2026-03-04 10:28:40 +11:00
Cillian O'Ruanaidh
67853bb976 Use guard 2026-03-04 10:28:39 +11:00
Cillian O'Ruanaidh
d57274fc4c Manager invitation email is only for new users 2026-03-04 10:28:39 +11:00
Cillian O'Ruanaidh
f063e2e8c6 Change to email field 2026-03-04 10:28:39 +11:00
Cillian O'Ruanaidh
d3eb887664 Align button 2026-03-04 10:28:39 +11:00
Cillian O'Ruanaidh
efae11e2af Change assertion for flakey test failure 2026-03-04 10:28:39 +11:00
Cillian O'Ruanaidh
1554459eb9 Display a JS alert if /admin/search/known_users returns an error 2026-03-04 10:28:39 +11:00
Cillian O'Ruanaidh
50265780cf Call AdminTooltipComponent directly and remove unnecessary partial 2026-03-04 10:28:39 +11:00
Cillian O'Ruanaidh
7433f6d183 Rename :save to save! on UserInvitation because it possibly could raise an exception 2026-03-04 10:28:39 +11:00
Cillian O'Ruanaidh
f1071575cd Remove Angular from Enterprise > Settings > Users section 2026-03-04 10:28:36 +11:00
Maikel
7a4beb8b22 Merge pull request #13982 from openfoodfoundation/dependabot/npm_and_yarn/tom-select-2.5.2
Bump tom-select from 2.5.1 to 2.5.2
2026-03-04 09:49:28 +11:00
Maikel
9a48ee16cc Merge pull request #13983 from openfoodfoundation/dependabot/npm_and_yarn/hotkeys-js-4.0.1
Bump hotkeys-js from 4.0.0 to 4.0.1
2026-03-04 09:48:23 +11:00
Maikel
50c0e8af7d Merge pull request #13984 from openfoodfoundation/dependabot/bundler/pdf-reader-2.15.1
Bump pdf-reader from 2.15.0 to 2.15.1
2026-03-04 09:47:35 +11:00
David Cook
1cf2928f9f Merge pull request #13979 from openfoodfoundation/RachL-patch-1
Update release issue template
2026-03-04 09:10:24 +11:00
François Turbelin
6cacde837d Remove duplicated test 2026-03-03 14:27:20 +01:00
dependabot[bot]
1d2d661675 Bump pdf-reader from 2.15.0 to 2.15.1
Bumps [pdf-reader](https://github.com/yob/pdf-reader) from 2.15.0 to 2.15.1.
- [Changelog](https://github.com/yob/pdf-reader/blob/main/CHANGELOG)
- [Commits](https://github.com/yob/pdf-reader/compare/v2.15.0...v2.15.1)

---
updated-dependencies:
- dependency-name: pdf-reader
  dependency-version: 2.15.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 09:33:47 +00:00
dependabot[bot]
5029c03205 Bump hotkeys-js from 4.0.0 to 4.0.1
Bumps [hotkeys-js](https://github.com/jaywcjlove/hotkeys-js) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/jaywcjlove/hotkeys-js/releases)
- [Commits](https://github.com/jaywcjlove/hotkeys-js/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: hotkeys-js
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 09:33:32 +00:00
dependabot[bot]
2b648f3f3c Bump tom-select from 2.5.1 to 2.5.2
Bumps [tom-select](https://github.com/orchidjs/tom-select) from 2.5.1 to 2.5.2.
- [Release notes](https://github.com/orchidjs/tom-select/releases)
- [Commits](https://github.com/orchidjs/tom-select/compare/v2.5.1...2.5.2)

---
updated-dependencies:
- dependency-name: tom-select
  dependency-version: 2.5.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 09:33:16 +00:00
David Cook
b2e847b551 Merge pull request #13957 from openfoodfoundation/dependabot/bundler/i18n-1.14.8
Bump i18n from 1.14.7 to 1.14.8
2026-03-03 17:16:58 +11:00
Maikel Linke
4873fd3275 Remove non-task item 2026-03-03 16:16:57 +11:00
Maikel Linke
e0ad4363a9 Fix formatting 2026-03-03 16:12:47 +11:00
David Cook
46de21ea2b Restore required expectation
It is the whole purpose of the spec.
2026-03-03 11:06:10 +11:00
Gaetan Craig-Riou
efdbf25f86 Merge pull request #13248 from drummer83/email_whitelabel
White labelling all customer facing emails
2026-03-03 09:40:47 +11:00
François Turbelin
a069e4247f Remove helper method to use direct logic 2026-03-02 22:43:18 +01:00
François Turbelin
7010cda9f7 Tidy up the tests 2026-03-02 22:07:45 +01:00
Konrad
498ed5a3ec Resolve conflicts 2026-03-02 22:05:29 +01:00
Rachel Arnould
c7d4c6f3c4 Merge pull request #13835 from prikeshsavla/13569-remove-v3-admin-styles
Refactor admin CSS: Promote v3 to canonical admin styles
2026-03-02 12:29:33 +01:00
Rachel Arnould
70b2a6d999 Update release template
Currently testers are using google docs to store release tests. This is maybe overkill.

I've documented the steps here and the proposal is for testers to document their test in the release issue.
2026-03-02 12:22:11 +01:00
dependabot[bot]
36e3e16ba0 Bump i18n from 1.14.7 to 1.14.8
Bumps [i18n](https://github.com/ruby-i18n/i18n) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/ruby-i18n/i18n/releases)
- [Changelog](https://github.com/ruby-i18n/i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ruby-i18n/i18n/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: i18n
  dependency-version: 1.14.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 02:36:18 +00:00
Gaetan Craig-Riou
0f047e2c25 Merge pull request #13977 from openfoodfoundation/dependabot/npm_and_yarn/minimatch-3.1.5
Bump minimatch from 3.1.2 to 3.1.5
2026-03-02 12:00:02 +11:00
dependabot[bot]
ef7bd083ed Bump minimatch from 3.1.2 to 3.1.5
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 03:02:06 +00:00
Rachel Arnould
c13785f2e3 Merge pull request #13943 from pavelk-lab/replace-brand-story-angular-with-details
Replace Angular expand/collapse with native HTML <details>/<summary> for brand story
2026-02-27 15:37:00 +01:00
François Turbelin
28dde86960 Repair ProxyOrder to support order cycle without closing time 2026-02-27 14:36:20 +01:00
François Turbelin
08691f81a1 Adjust logic and specs 2026-02-27 14:09:44 +01:00
Gaetan Craig-Riou
0c0304b1c1 Merge pull request #13976 from openfoodfoundation/revert-13973-fix-rubocop-violations
Revert "Fix 9 Rubocop violations (Rails/Presence and Rails/RedirectBackOrTo)Fix rubocop violations"
2026-02-27 10:06:29 +11:00
David-OFN-CA
7922bf7b65 Revert "Fix 9 Rubocop violations (Rails/Presence and Rails/RedirectBackOrTo)Fix rubocop violations" 2026-02-26 17:04:21 -05:00
David-OFN-CA
2d46676bb4 Merge pull request #13973 from David-OFN-CA/fix-rubocop-violations
Fix 9 Rubocop violations (Rails/Presence and Rails/RedirectBackOrTo)Fix rubocop violations
2026-02-26 16:01:15 -05:00
David Thomas
2808a41f0d Safely autocorrect Rails/RedirectBackOrTo
Inspecting 1721 files
........................................W...................................................................W................W..W.......W................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

Offenses:

app/controllers/admin/order_cycles_controller.rb:212:9: W: [Corrected] Rails/RedirectBackOrTo: Use redirect_back_or_to instead of redirect_back with :fallback_location keyword argument.
        redirect_back(fallback_location: root_path)
        ^^^^^^^^^^^^^
app/controllers/locales_controller.rb:6:5: W: [Corrected] Rails/RedirectBackOrTo: Use redirect_back_or_to instead of redirect_back with :fallback_location keyword argument.
    redirect_back fallback_location: main_app.root_url
    ^^^^^^^^^^^^^
app/controllers/spree/admin/invoices_controller.rb:31:9: W: [Corrected] Rails/RedirectBackOrTo: Use redirect_back_or_to instead of redirect_back with :fallback_location keyword argument.
        redirect_back(fallback_location: spree.admin_dashboard_path)
        ^^^^^^^^^^^^^
app/controllers/spree/admin/orders_controller.rb:83:9: W: [Corrected] Rails/RedirectBackOrTo: Use redirect_back_or_to instead of redirect_back with :fallback_location keyword argument.
        redirect_back fallback_location: spree.admin_dashboard_path
        ^^^^^^^^^^^^^
app/controllers/spree/admin/orders_controller.rb:91:25: W: [Corrected] Rails/RedirectBackOrTo: Use redirect_back_or_to instead of redirect_back with :fallback_location keyword argument.
          format.html { redirect_back(fallback_location: spree.admin_dashboard_path) }
                        ^^^^^^^^^^^^^
app/controllers/spree/admin/return_authorizations_controller.rb:13:9: W: [Corrected] Rails/RedirectBackOrTo: Use redirect_back_or_to instead of redirect_back with :fallback_location keyword argument.
        redirect_back fallback_location: spree.admin_dashboard_path
        ^^^^^^^^^^^^^

1721 files inspected, 6 offenses detected, 6 offenses corrected
2026-02-26 15:35:55 -05:00
David Thomas
18869979db Safely autocorrect Rails/Presence
Inspecting 1721 files
...................................C.................................................................................................................................................................................................................................................................................................................................................C...................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

Offenses:

app/controllers/admin/enterprises_controller.rb:180:7: C: [Corrected] Rails/Presence: Use @object.custom_tab.presence&.destroy instead of @object.custom_tab.destroy if @object.custom_tab.present?.
      @object.custom_tab.destroy if @object.custom_tab.present?
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
app/controllers/admin/enterprises_controller.rb:243:9: C: [Corrected] Rails/Presence: Use (enterprises.presence&.includes(supplied_products: [:variants, :image])) instead of if enterprises.present? ... end.
        if enterprises.present? ...
        ^^^^^^^^^^^^^^^^^^^^^^^
app/controllers/admin/enterprises_controller.rb:243:9: C: [Corrected] Style/RedundantParentheses: Don't use parentheses around a method call.
        (enterprises.presence&.includes(supplied_products: [:variants, :image]))
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
app/models/spree/product.rb:298:7: C: [Corrected] Rails/Presence: Use (first_variant.supplier.presence&.touch) instead of first_variant.supplier.touch if first_variant.supplier.present?.
      first_variant.supplier.touch if first_variant.supplier.present?
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
app/models/spree/product.rb:298:7: C: [Corrected] Style/RedundantParentheses: Don't use parentheses around a method call.
      (first_variant.supplier.presence&.touch)
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

1721 files inspected, 5 offenses detected, 5 offenses corrected
2026-02-26 15:34:45 -05:00
David Thomas
708dbb2270 Regenerate Rubocop's TODO file 2026-02-26 15:33:37 -05:00
François Turbelin
de52e21ee9 Remove floating point overkill for prices sum 2026-02-26 18:04:02 +01:00
Zil Norvilis
9488e9b459 feat: display credit card brand instead of card type in the saved cards list. 2026-02-26 15:52:45 +02:00
Zil Norvilis
503429960a Reverse credit_card_serializer changes 2026-02-26 15:10:21 +02:00
Rachel Arnould
83ec97e720 Merge pull request #13944 from pavelk-lab/fix/rails-7-serialize-deprecation
Fix Rails 7.2 serialize deprecation warnings
2026-02-26 11:29:27 +01:00
Maikel Linke
fd178ee80b Use unique categories to avoid flakiness 2026-02-26 11:42:46 +11:00
Maikel Linke
e4db20f86e Remove unncessary expectation
This assertion was confusing me. It was quite complex and the only thing
it was asserting was the placeholder via a CSS selector. I don't think
it's worth keeping.
2026-02-26 11:37:30 +11:00
Zil Norvilis
58520a0c4c test: Add specs for the stripe_card_options helper method, verifying card formatting and month padding. 2026-02-25 19:49:10 +02:00
Zil Norvilis
0bc4b1c885 refactor: Standardize credit card type attribute to cc_type across the application, removing the brand alias and related methods. 2026-02-25 19:35:53 +02:00
Arun Guleria
b7a1754879 13864 - remove unnecessary routes for update positions 2026-02-25 16:29:20 +05:30
Maikel Linke
560348722c Revert "Test current flakiness of spec"
This reverts commit 7b715bf6c7.
2026-02-25 15:18:39 +11:00
Maikel Linke
6d17cf50fb Test impact of longer wait time on flakiness 2026-02-25 15:08:50 +11:00
Maikel Linke
7b715bf6c7 Test current flakiness of spec 2026-02-25 15:07:52 +11:00
Arun Guleria
ab811b2c83 13864-Remove dead feature to re-arrange variants 2026-02-24 18:16:34 +05:30
Pavel
85c903cb7f Remove fixed serialize deprecation from allowed warnings
The "Passing the class as positional argument" warning was suppressed
while serialize calls were being updated to use the keyword argument
form. Now that the fix is applied, the suppression is no longer needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 23:30:48 +00:00
Zil Norvilis
2cfd386ad7 test: add spec for Spree::CreditCard#brand= setter to verify card type reformatting 2026-02-22 22:28:40 +02:00
Zil Norvilis
ce94b394b2 feat: Add brand= setter to Spree::CreditCard for cc_type assignment. 2026-02-22 22:01:54 +02:00
Pavel
98775bfdb8 Pass type as keyword argument in migration serialize call
Same fix as applied to Invoice and ReportRenderingOptions models in
the parent PR: Rails 7.2 requires the type class to be passed as a
keyword argument to serialize.

  serialize :options, Hash, coder: YAML
  ->
  serialize :options, type: Hash, coder: YAML

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 23:23:12 +00:00
pavelk-lab
47ef21deb3 Merge branch 'master' into fix/rails-7-serialize-deprecation 2026-02-21 23:19:58 +00:00
Pavel
e98244fe63 Fix Rails 7.2 serialize deprecation warnings
Pass type as keyword argument in serialize calls, as required from Rails 7.2 onwards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 21:55:13 +00:00
Pavel
b348536d12 Clean up dead code after Angular brand story removal
- Delete HomeCtrl controller file (nothing references it anymore)
- Remove misplaced .text-vbig class from <details> (was on the old <a> toggle; summary has no visible text)
- Remove redundant cursor: pointer on #brand-story-text (<summary> already sets it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 21:39:32 +00:00
Pavel
b528bb47a0 Replace Angular expand/collapse with native HTML details/summary for brand story
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 21:23:09 +00:00
Prikesh Savla
5a9aa87831 Rename admin-style-v3 to admin-style and remove the admin_legacy styles folder that has no references 2026-01-25 21:15:40 +05:30
Prikesh Savla
8cfab08f9e Refactor admin CSS: Promote v3 to canonical admin styles
The Admin V3 styles are now the primary styles for the application. This change promotes the `admin_v3` directory to `admin` and archives the old styles.

Changes:
- Renamed `app/webpacker/css/admin_v3` to `app/webpacker/css/admin`.
- Renamed the previous `app/webpacker/css/admin` to `app/webpacker/css/admin_legacy`.
- Moved all files referenced by V3 styles from the legacy directory to the new `admin` directory.
- Updated imports in `all.scss` to distinct local files instead of relative paths.
- Cleaned up `admin_legacy` by removing files that are duplicates (by name or content) of the new admin styles.
- Updated `admin-style-v3` pack to point to the new location.
2026-01-25 21:05:38 +05:30
Konrad
e814fdd447 Remove test for active white labeling in test email as proposed by David 2025-12-29 12:36:50 +01:00
David Cook
4d6231105f Assign attribute directly instead of mocking
It's better to set a variable the same way the real code would

Co-authored-by: Maikel <maikel@email.org.au>
2025-12-29 12:18:55 +01:00
Konrad
3a75c3446e Update backorder mailer specs to check that these hub facing emails remain unaffected by white labelling (there are no customer facing emails here)
Also make use of the newly separated shared_examples
2025-12-29 12:18:55 +01:00
Konrad
29a76fd721 Update report mailer specs to check that this enterprise facing email remains unaffected by white labelling (there are no customer facing emails here)
Also make use of the newly separated shared_examples
2025-12-29 12:18:55 +01:00
Konrad
593bd89095 Update test mailer specs to check that this super admin facing email remains unaffected by white labelling (there are no user or customer facing emails here)
Also make use of the newly separated shared_examples
2025-12-29 12:18:55 +01:00
Konrad
0079ed219b Update producer mailer specs to check that this producer facing email remains unaffected by white labelling (there are no customer facing emails here)
Also make use of the newly separated shared_examples
2025-12-29 12:18:55 +01:00
Konrad
5957d99812 Update enterprise mailer specs to check that enterprise facing emails remain unaffected by white labelling (there are no customer facing emails here)
Also make use of the newly separated shared_examples
2025-12-29 12:18:55 +01:00
Konrad
89453ec758 Update subscription mailer specs to check that customer facing emails are white labelled, while shop facing emails are not
Also make use of the newly separated shared_examples

Made the check for link to order page more general, because on my system a double quote was expected but a single quote was generated
2025-12-29 12:18:55 +01:00
Konrad
0de4f2f596 Update payment mailer specs to check that customer facing emails are white labelled, while shop facing emails are not
Also make use of the newly separated shared_examples
2025-12-29 12:18:55 +01:00
Konrad
183cbecef6 Update shipment mailer specs to check that customer facing emails are white labelled (there are no shop facing emails here)
Make use of the newly separated shared_examples
2025-12-29 12:18:55 +01:00
Konrad
ab6a49e568 Update order mailer specs to check that customer facing emails are white labelled, while shop facing emails are not
Also make use of the newly separated shared_examples

Including the invoice email was too tricky for me for now, sorry
2025-12-29 12:18:55 +01:00
Konrad
52ddb29dc7 Update user mailer specs to check that white labelling does not affect these emails
Move tests to separate file for reuse in other emails

Pass on :mail symbol and obtain the mail-object using public_send() to call it with different names
2025-12-29 12:18:54 +01:00
drummer83
5ce7905a33 White labelling ALL customer facing emails
White labelling added for Order: cancellation email, Order: invoice email, Shipment: shipped email, Subscriptions: authorize payment email, Subscriptions: placement email, Subscriptions: empty order email, Subscriptions: failed payment email

White labelling existed already for Order: confirmation email, Subscriptions: order confirmation email
2025-12-29 12:18:54 +01:00
François Turbelin
8748bd76e2 Introduce a prices_sum helper to factorize the calculation on reports 2025-12-18 18:00:23 +01:00
François Turbelin
30f702ea96 Repair rounding issue on totals in reports 2025-12-04 16:14:04 +01:00
280 changed files with 4042 additions and 2827 deletions

View File

@@ -27,7 +27,12 @@ assignees: ''
- [ ] Move this issue to Test Ready.
- [ ] Notify `@testers` in [#testing].
- [ ] Test build: [Deploy to Staging] with release tag.
- [ ] Notify a deployer to deploy it
- [ ] Map is displayed correctly. Address changes are reflected in the map.
- [ ] Stripe with no authentication card: `4242424242424242` as shopper and as Admin. Order confirmation displays order as "Paid".
- [ ] Stripe with Authentication required card: `4000002760003184` as shopper and as Admin. As admin, check authorization through customer account `/account#/transactions` and email.
- [ ] Pay with Paypal.
- [ ] Order on mobile.
- [ ] Notify a deployer to deploy it.
## 3. Deployment at beginning of week
@@ -57,4 +62,4 @@ The full process is described at https://github.com/openfoodfoundation/openfoodn
[Create issue]: https://github.com/openfoodfoundation/openfoodnetwork/issues/new?assignees=&labels=&projects=&template=release.md&title=Release
[#delivery-circle]: https://openfoodnetwork.slack.com/archives/C01T75H6G0Z
[Transifex Client]: https://developers.transifex.com/docs/cli
[minor or major breaking changes]: https://github.com/openfoodfoundation/openfoodnetwork/pulls?q=label%3A%22breaking+change%22%2C%22major+breaking+change%22
[minor or major breaking changes]: https://github.com/openfoodfoundation/openfoodnetwork/pulls?q=label%3A%22breaking+change%22%2C%22major+breaking+change%22

View File

@@ -203,6 +203,7 @@ group :development do
gem 'spring'
gem 'spring-commands-rspec'
gem 'spring-commands-rubocop'
gem 'spring-watcher-listen'
gem 'web-console'
gem 'rack-mini-profiler', '< 3.0.0'

View File

@@ -170,7 +170,7 @@ GEM
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
aes_key_wrap (1.1.0)
afm (0.2.2)
afm (1.0.0)
angular-rails-templates (1.4.0)
railties (>= 5.0, < 8.2)
sprockets (>= 3.0, < 5)
@@ -313,7 +313,7 @@ GEM
eventmachine (>= 1.0.0.beta.1)
email_validator (2.2.4)
activemodel
erb (6.0.1)
erb (6.0.2)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
@@ -405,7 +405,7 @@ GEM
reline
htmlentities (4.4.2)
http_parser.rb (0.8.0)
i18n (1.14.7)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
i18n-js (3.9.2)
i18n (>= 0.6.6)
@@ -507,7 +507,8 @@ GEM
logger
mini_mime (1.1.5)
mini_portile2 (2.8.6)
minitest (6.0.1)
minitest (6.0.2)
drb (~> 2.0)
prism (~> 1.5)
monetize (1.13.0)
money (~> 6.12)
@@ -584,7 +585,7 @@ GEM
xml-simple
paypal-sdk-merchant (1.117.2)
paypal-sdk-core (~> 0.3.0)
pdf-reader (2.15.0)
pdf-reader (2.15.1)
Ascii85 (>= 1.0, < 3.0, != 2.0.0)
afm (>= 0.2.1, < 2)
hashery (~> 2.0)
@@ -670,8 +671,8 @@ GEM
activesupport (>= 4.2)
choice (~> 0.2.0)
ruby-graphviz (~> 1.2)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (7.0.10)
i18n (>= 0.7, < 2)
@@ -857,6 +858,9 @@ GEM
spring (>= 0.9.1)
spring-commands-rubocop (0.4.0)
spring (>= 1.0)
spring-watcher-listen (2.1.0)
listen (>= 2.7, < 4.0)
spring (>= 4)
sprockets (3.7.5)
base64
concurrent-ruby (~> 1.0)
@@ -904,7 +908,7 @@ GEM
tsort (0.2.0)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
turbo-rails (2.0.20)
turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
turbo_power (0.7.0)
@@ -932,8 +936,7 @@ GEM
public_suffix
validates_lengths_from_database (0.8.0)
activerecord (>= 4)
vcr (6.3.1)
base64
vcr (6.4.0)
view_component (4.1.1)
actionview (>= 7.1.0, < 8.2)
activesupport (>= 7.1.0, < 8.2)
@@ -970,7 +973,7 @@ GEM
xml-simple (1.1.8)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.4)
zeitwerk (2.7.5)
PLATFORMS
ruby
@@ -1101,6 +1104,7 @@ DEPENDENCIES
spring
spring-commands-rspec
spring-commands-rubocop
spring-watcher-listen
sprockets (~> 3.7)
state_machines-activerecord
stimulus_reflex

View File

@@ -4,16 +4,12 @@ angular.module("admin.enterprises")
$scope.Enterprises = Enterprises
$scope.navClear = NavigationCheck.clear
$scope.menu = SideMenu
$scope.newManager = { id: null, email: (t('add_manager')) }
$scope.StatusMessage = StatusMessage
$scope.RequestMonitor = RequestMonitor
$scope.$watch 'enterprise_form.$dirty', (newValue) ->
StatusMessage.display 'notice', t('admin.unsaved_changes') if newValue
$scope.$watch 'newManager', (newValue) ->
$scope.addManager($scope.newManager) if newValue
$scope.setFormDirty = ->
$scope.$apply ->
$scope.enterprise_form.$setDirty()
@@ -35,26 +31,6 @@ angular.module("admin.enterprises")
# Register the NavigationCheck callback
NavigationCheck.register(enterpriseNavCallback)
$scope.removeManager = (manager) ->
if manager.id?
if manager.id == $scope.Enterprise.owner.id or manager.id == parseInt($scope.receivesNotifications)
return
for i, user of $scope.Enterprise.users when user.id == manager.id
$scope.Enterprise.users.splice i, 1
$scope.enterprise_form?.$setDirty()
$scope.addManager = (manager) ->
if manager.id? and angular.isNumber(manager.id) and manager.email?
manager =
id: manager.id
email: manager.email
confirmed: manager.confirmed
if (user for user in $scope.Enterprise.users when user.id == manager.id).length == 0
$scope.Enterprise.users.unshift(manager)
$scope.enterprise_form?.$setDirty()
else
alert ("#{manager.email}" + " " + t("is_already_manager"))
$scope.performEnterpriseAction = (enterpriseActionName, warning_message_key, success_message_key) ->
return unless confirm($scope.translation(warning_message_key))

View File

@@ -1,5 +0,0 @@
angular.module('Darkswarm').controller "HomeCtrl", ($scope) ->
$scope.brandStoryExpanded = false
$scope.toggleBrandStory = ->
$scope.brandStoryExpanded = !$scope.brandStoryExpanded

View File

@@ -9,6 +9,11 @@
padding: 1.2rem;
}
&.big {
max-height: 50em;
overflow: auto;
}
h1,
h2,
h3,

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
module Admin
class CustomerAccountTransactionController < Admin::ResourceController
def index
@available_credit = @collection.first&.balance || 0.00
respond_with do |format|
format.turbo_stream {
render :index
}
end
end
# We are using an old version of CanCanCan so I could not get `accessible_by` to work properly,
# so we are doing our own authorization before calling 'accessible_by'
def collection
allowed_customers = OpenFoodNetwork::Permissions.new(spree_current_user)
.managed_enterprises.joins(:customers).select("customers.id").map(&:id)
raise CanCan::AccessDenied unless allowed_customers.include?(params[:customer_id].to_i)
CustomerAccountTransaction.accessible_by(current_ability, action)
.where(customer_id: params[:customer_id]).order(id: :desc)
end
end
end

View File

@@ -67,7 +67,6 @@ module Admin
def update
tag_rules_attributes = params[object_name].delete :tag_rules_attributes
update_tag_rules(tag_rules_attributes) if tag_rules_attributes.present?
update_enterprise_notifications
update_vouchers
delete_custom_tab if params[:custom_tab] == 'false'
@@ -163,9 +162,11 @@ module Admin
end
def destroy
if @object.destroy
@object.transaction do
@object.destroy!
flash.now[:success] = flash_message_for(@object, :successfully_removed)
else
rescue StandardError
Rails.logger.error @object.errors.full_messages.to_sentence
flash.now[:error] = @object.errors.full_messages.to_sentence
end
@@ -177,7 +178,7 @@ module Admin
protected
def delete_custom_tab
@object.custom_tab.destroy if @object.custom_tab.present?
@object.custom_tab.presence&.destroy
enterprise_params.delete(:custom_tab_attributes)
end
@@ -240,9 +241,7 @@ module Admin
enterprises = OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, @order_cycle)
.visible_enterprises
if enterprises.present?
enterprises.includes(supplied_products: [:variants, :image])
end
enterprises.presence&.includes(supplied_products: [:variants, :image])
when :index
if spree_current_user.admin?
OpenFoodNetwork::Permissions.new(spree_current_user).
@@ -314,14 +313,6 @@ module Admin
end
end
def update_enterprise_notifications
user_id = params[:receives_notifications].to_i
return unless user_id.positive? && @enterprise.user_ids.include?(user_id)
@enterprise.update_contact(user_id)
end
def update_vouchers
params_voucher_ids = params[:enterprise][:voucher_ids].to_a.map(&:to_i)
voucher_ids = @enterprise.vouchers.map(&:id)

View File

@@ -0,0 +1,31 @@
# frozen_string_literal: true
module Admin
class UserInvitationsController < ResourceController
before_action :load_enterprise
def new; end
def create
@user_invitation.attributes = permitted_resource_params
if @user_invitation.save!
flash[:success] = I18n.t(:user_invited, email: @user_invitation.email)
else
render :new
end
end
private
def load_enterprise
@enterprise = OpenFoodNetwork::Permissions
.new(spree_current_user)
.editable_enterprises
.find_by(permalink: params[:enterprise_id])
end
def permitted_resource_params
params.require(:user_invitation).permit(:email).merge(enterprise: @enterprise)
end
end
end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
module Api
module V1
class CustomerAccountTransactionController < Api::V1::BaseController
def create
authorize! :create, CustomerAccountTransaction
default_params = {
currency: CurrentConfig.get(:currency), created_by: current_api_user
}
parameters = default_params.merge(customer_account_transaction_params).merge(description: )
transaction = CustomerAccountTransaction.new(parameters)
if transaction.save
render json: Api::V1::CustomerAccountTransactionSerializer.new(transaction),
status: :created
else
invalid_resource! transaction
end
end
private
def customer_account_transaction_params
params.require(:customer_account_transaction).permit(:customer_id, :amount, :description)
end
def description
I18n.t(".api_customer_credit", description: params[:description])
end
end
end
end

View File

@@ -31,6 +31,11 @@ class CheckoutController < BaseController
check_step
end
if payment_step? || summary_step?
credit_payment_method = @order.distributor.payment_methods.customer_credit
@paid_with_credit = @order.payments.find_by(payment_method: credit_payment_method)&.amount
end
return if available_shipping_methods.any?
flash[:error] = I18n.t('checkout.errors.no_shipping_methods_available')
@@ -121,7 +126,9 @@ class CheckoutController < BaseController
shipping_method_updated = @order.shipping_method&.id != params[:shipping_method_id].to_i
@order.select_shipping_method(params[:shipping_method_id])
@order.update(order_params)
# We need to update voucher to take into account:
# * when moving away from "details" step : potential change in shipping method fees
# * when moving away from "payment" step : payment fees

View File

@@ -1,19 +0,0 @@
# frozen_string_literal: true
module ManagerInvitations
extend ActiveSupport::Concern
def create_new_manager(email, enterprise)
password = SecureRandom.base58(64)
new_user = Spree::User.create(email:, unconfirmed_email: email, password:)
new_user.reset_password_token = Devise.friendly_token
# Same time as used in Devise's lib/devise/models/recoverable.rb.
new_user.reset_password_sent_at = Time.now.utc
if new_user.save
enterprise.users << new_user
EnterpriseMailer.manager_invitation(enterprise, new_user).deliver_later
end
new_user
end
end

View File

@@ -15,6 +15,7 @@ module Spree
Spree::Gateway::StripeSCA
Spree::PaymentMethod::Check
Spree::PaymentMethod::Taler
Spree::PaymentMethod::CustomerCredit
}.freeze
def create

View File

@@ -5,7 +5,7 @@ module Spree
class PaymentsController < Spree::Admin::BaseController
before_action :load_order, except: [:show]
before_action :load_payment, only: [:fire, :show]
before_action :load_data
before_action :load_data, except: [:credit_customer]
before_action :can_transition_to_payment
# We ensure that items are in stock before all screens if the order is in the Payment state.
# This way, we don't allow someone to enter credit card details for an order only to be told
@@ -92,6 +92,18 @@ module Spree
end
end
def credit_customer
response = ::Orders::CustomerCreditService.new(@order).refund
if response.success?
flash[:success] = Spree.t(:customer_credit_successful, scope: "admin.payments")
else
flash[:error] = response.message
end
redirect_to admin_order_payments_path(@order)
end
private
def load_payment_source
@@ -185,7 +197,7 @@ module Spree
end
def allowed_events
%w{capture void_transaction credit refund resend_authorization_email
%w{capture void_transaction credit refund internal_void resend_authorization_email
capture_and_complete_order}
end

View File

@@ -29,7 +29,13 @@ module Spree
hide_ofn_navigation(@order.distributor)
end
def show; end
def show
credit_payment_method = @order.distributor.payment_methods.customer_credit
credit_payment = @order.payments.find_by(payment_method: credit_payment_method)
@paid_with_credit = credit_payment&.amount
@payment_total = @order.payment_total - @paid_with_credit.to_f
end
def empty
if @order = current_order

View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
class UserInvitation
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations::Callbacks
attribute :enterprise
attribute :email
before_validation :normalize_email
validates :email, presence: true, 'valid_email_2/email': { mx: true }
validates :enterprise, presence: true
validate :not_existing_enterprise_user
def save!
return unless valid?
user = find_or_create_user!
enterprise.users << user
return unless user.previously_new_record?
EnterpriseMailer.manager_invitation(enterprise, user).deliver_later
end
private
def find_or_create_user!
Spree::User.find_or_create_by!(email: email) do |user|
user.email = email
user.password = SecureRandom.base58(64)
user.unconfirmed_email = email
user.reset_password_token = Devise.friendly_token
# Same time as used in Devise's lib/devise/models/recoverable.rb.
user.reset_password_sent_at = Time.now.utc
end
end
def normalize_email
self.email = email.strip if email.present?
end
def not_existing_enterprise_user
return unless email.present? && enterprise.users.where(email: email).exists?
errors.add(:email, :is_already_manager)
end
end

View File

@@ -83,31 +83,6 @@ module CheckoutHelper
Spree::Money.new order.total - order.total_tax, currency: order.currency
end
def validated_input(name, path, args = {})
attributes = {
:required => true,
:type => :text,
:name => path,
:id => path,
"ng-model" => path,
"ng-class" => "{error: !fieldValid('#{path}')}"
}.merge args
render "shared/validated_input", name:, path:, attributes:
end
def validated_select(name, path, options, args = {})
attributes = {
:required => true,
:id => path,
"ng-model" => path,
"ng-class" => "{error: !fieldValid('#{path}')}"
}.merge args
render "shared/validated_select", name:, path:, options:,
attributes:
end
def payment_method_price(method, order)
price = method.compute_amount(order)
if price == 0
@@ -139,7 +114,7 @@ module CheckoutHelper
def stripe_card_options(cards)
cards.map do |cc|
[
"#{cc.brand} #{cc.last_digits} #{I18n.t(:card_expiry_abbreviation)}:" \
"#{cc.cc_type} #{cc.last_digits} #{I18n.t(:card_expiry_abbreviation)}:" \
"#{cc.month.to_s.rjust(2, '0')}/#{cc.year}", cc.id
]
end

View File

@@ -17,7 +17,7 @@ module ReportsHelper
next unless payment_method
[payment_method.name, payment_method.id]
[payment_method.display_name, payment_method.id]
end.compact.uniq
end

View File

@@ -11,7 +11,7 @@ module Spree
end
def payment_method_name(payment)
payment_method(payment)&.name
payment_method(payment)&.display_name
end
end
end

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
class CustomerAccountTransactionSchema < JsonApiSchema
def self.object_name
"customer_account_transaction"
end
def self.attributes
{
id: { type: :integer, example: 1 },
customer_id: { type: :integer, example: 10 },
amount: { type: :decimal, example: 10.50 },
currency: { type: :string, example: "AUD" },
description: { type: :string, nullable: true, example: "Payment processed by POS" },
balance: { type: :decimal, example: 10.50 },
}
end
def self.required_attributes
[:customer_id, :amount]
end
def self.writable_attributes
attributes.except(:id, :balance, :currency)
end
def self.relationships
[:customer]
end
end

View File

@@ -6,6 +6,7 @@ class PaymentMailer < ApplicationMailer
def authorize_payment(payment)
@payment = payment
@order = @payment.order
@hide_ofn_navigation = @payment.order.distributor.hide_ofn_navigation
I18n.with_locale valid_locale(@order.user) do
mail(to: @order.email,
subject: default_i18n_subject(distributor: @order.distributor.name),

View File

@@ -11,6 +11,7 @@ module Spree
def cancel_email(order_or_order_id, resend = false)
@order = find_order(order_or_order_id)
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
I18n.with_locale valid_locale(@order.user) do
mail(to: @order.email,
subject: mail_subject(t('spree.order_mailer.cancel_email.subject'), resend),
@@ -51,6 +52,7 @@ module Spree
def invoice_email(order_or_order_id, options = {})
@order = find_order(order_or_order_id)
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
current_user = if options[:current_user_id].present?
find_user(options[:current_user_id])
end

View File

@@ -4,6 +4,7 @@ module Spree
class ShipmentMailer < ApplicationMailer
def shipped_email(shipment, delivery:)
@shipment = shipment.respond_to?(:id) ? shipment : Spree::Shipment.find(shipment)
@hide_ofn_navigation = @shipment.order.distributor.hide_ofn_navigation
@delivery = delivery
@order = @shipment.order
subject = base_subject

View File

@@ -19,6 +19,7 @@ class SubscriptionMailer < ApplicationMailer
@type = 'empty'
@changes = changes
@order = order
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
send_mail(order)
end
@@ -26,11 +27,13 @@ class SubscriptionMailer < ApplicationMailer
@type = 'placement'
@changes = changes
@order = order
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
send_mail(order)
end
def failed_payment_email(order)
@order = order
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
send_mail(order)
end

View File

@@ -19,6 +19,7 @@ class Customer < ApplicationRecord
belongs_to :enterprise
belongs_to :user, class_name: "Spree::User", optional: true
has_many :orders, class_name: "Spree::Order", dependent: :nullify
has_many :customer_account_transactions, dependent: :restrict_with_error
before_validation :downcase_email
before_validation :empty_code
before_create :associate_user

View File

@@ -0,0 +1,38 @@
# frozen_string_literal: true
require "spree/localized_number"
class CustomerAccountTransaction < ApplicationRecord
extend Spree::LocalizedNumber
localize_number :amount
belongs_to :customer
belongs_to :payment, class_name: "Spree::Payment", optional: true
belongs_to :created_by, class_name: "Spree::User", optional: true
validates :amount, presence: true
validates :currency, presence: true
before_create :update_balance
private
def readonly?
!new_record?
end
def update_balance
# Locking the customer to prevent two transactions from behing created at the same time
# resulting in a potentially wrong balance calculation.
customer.with_lock(requires_new: true) do
last_transaction = CustomerAccountTransaction.where(customer: customer).last
self.balance = if last_transaction.present?
last_transaction.balance + amount
else
amount
end
end
end
end

View File

@@ -294,7 +294,13 @@ class Enterprise < ApplicationRecord
contact || owner
end
def update_contact(user_id)
def contact_id
contact&.id
end
def contact_id=(user_id)
return unless user_id.to_i.positive? && users.confirmed.exists?(user_id.to_i)
enterprise_roles.update_all(["receives_notifications=(user_id=?)", user_id])
end
@@ -576,7 +582,7 @@ class Enterprise < ApplicationRecord
end
def set_default_contact
update_contact owner_id
self.contact_id = owner_id
end
def relate_to_owners_enterprises

View File

@@ -4,7 +4,7 @@ class Invoice < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :order, class_name: 'Spree::Order'
serialize :data, Hash, coder: YAML
serialize :data, type: Hash, coder: YAML
before_validation :serialize_order
after_create :cancel_previous_invoices
default_scope { order(created_at: :desc) }

View File

@@ -18,7 +18,7 @@ class Invoice
end
def payment_method_name
payment_method&.name
payment_method&.display_name
end
end
end

View File

@@ -3,7 +3,7 @@
class Invoice
class DataPresenter
class PaymentMethod < Invoice::DataPresenter::Base
attributes :id, :name, :description
attributes :id, :display_name, :display_description
invoice_generation_attributes :id
end
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class OrderBalance
delegate :negative?, :positive?, :zero?, :abs, :to_s, :to_f, :to_d, :<, :>, to: :amount
delegate :negative?, :positive?, :zero?, :abs, :to_s, :to_f, :to_d, :<, :>, :<=, :>=, to: :amount
def initialize(order)
@order = order

View File

@@ -76,6 +76,7 @@ class ProxyOrder < ApplicationRecord
def cart?
order&.state == 'complete' &&
order_cycle.orders_close_at.present? &&
order_cycle.orders_close_at > Time.zone.now
end

View File

@@ -4,5 +4,5 @@ class ReportRenderingOptions < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :user, class_name: "Spree::User"
serialize :options, Hash, coder: YAML
serialize :options, type: Hash, coder: YAML
end

View File

@@ -61,6 +61,7 @@ module Spree
add_manage_line_items_abilities user
end
add_relationship_management_abilities user if can_manage_relationships? user
add_customer_account_transaction_abilities user if can_manage_enterprises? user
end
# New users have no enterprises.
@@ -191,7 +192,7 @@ module Spree
user.enterprises.include? stripe_account.enterprise
end
can [:admin, :create], :manager_invitation
can [:admin, :create], UserInvitation
can [:admin, :index, :destroy], :oidc_setting
@@ -457,5 +458,9 @@ module Spree
user.enterprises.include?(enterprise_relationship.child)
end
end
def add_customer_account_transaction_abilities(_user)
can [:admin, :create, :index], CustomerAccountTransaction
end
end
end

View File

@@ -25,9 +25,6 @@ module Spree
scope :with_payment_profile, -> { where.not(gateway_customer_profile_id: nil) }
# needed for some of the ActiveMerchant gateways (eg. SagePay)
alias_attribute :brand, :cc_type
def expiry=(expiry)
self[:month], self[:year] = expiry.split(" / ")
self[:year] = "20#{self[:year]}"

View File

@@ -52,9 +52,9 @@ module Spree
def supports?(source)
return true unless provider_class.respond_to? :supports?
return false unless source.brand
return false unless source.cc_type
provider_class.supports?(source.brand)
provider_class.supports?(source.cc_type)
end
end
end

View File

@@ -730,5 +730,9 @@ module Spree
adjustment.update_adjustment!(force: true)
update_totals_and_states
end
def apply_customer_credit
Orders::CustomerCreditService.new(self).apply
end
end
end

View File

@@ -78,6 +78,8 @@ module Spree
before_transition to: :delivery, do: :create_proposed_shipments
before_transition to: :delivery, do: :ensure_available_shipping_rates
before_transition to: :payment, do: :apply_customer_credit
before_transition to: :confirmation, do: :validate_payment_method!
after_transition to: :payment do |order|

View File

@@ -18,7 +18,7 @@ module Spree
belongs_to :order, class_name: 'Spree::Order'
belongs_to :source, polymorphic: true
belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
belongs_to :payment_method, class_name: "Spree::PaymentMethod", inverse_of: :payments
has_many :offsets, -> { where("source_type = 'Spree::Payment' AND amount < 0").completed },
class_name: "Spree::Payment", foreign_key: :source_id,
@@ -115,12 +115,20 @@ module Spree
Alert.raise(
e,
metadata: {
event_tye: "ofn.payment_transition", payment_id: payment.id, event: transition.to
event_type: "ofn.payment_transition", payment_id: payment.id, event: transition.to
}
)
end
end
# Allows by passing the default scope on Spree::PaymentMethod. It's needed to link payment
# to internal payment method.
# Using ->{ unscoped } on the association doesn't work presumably because the default scope
# is not a simple `where`.
def payment_method
Spree::PaymentMethod.unscoped { super }
end
def money
Spree::Money.new(amount, currency:)
end

View File

@@ -4,6 +4,8 @@ module Spree
class Payment < ApplicationRecord
module Processing
def process!
return internal_purchase! if payment_method.internal?
return unless validate!
purchase!
@@ -20,6 +22,17 @@ module Spree
end
end
def internal_purchase!
started_processing!
options = { customer_id: order.customer_id, payment_id: id, order_number: order.number }
response = payment_method.purchase(
(amount * 100).round,
nil,
options
)
handle_response(response, :complete, :failure)
end
def authorize!(return_url = nil)
started_processing!
gateway_action(source, :authorize, :pend, return_url:)
@@ -131,6 +144,27 @@ module Spree
end
end
def internal_void!
return true if void?
# We should only void complete payment, otherwise we will be refunding credit that was
# not used in the first place.
return gateway_error(Spree.t(:internal_payment_not_voidable)) if state != "completed"
options = { customer_id: order.customer_id, payment_id: id, order_number: order.number }
response = payment_method.void(
(amount * 100).round,
nil,
options
)
record_response(response)
if response.success?
void
else
gateway_error(response)
end
end
def partial_credit(amount)
return if amount > credit_allowed
@@ -248,6 +282,7 @@ module Spree
end
logger.error(Spree.t(:gateway_error))
logger.error(" #{error.to_yaml}")
# TODO why is this not captured ?
raise Core::GatewayError, text
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Spree
class PaymentMethod < ApplicationRecord
class PaymentMethod < ApplicationRecord # rubocop:disable Metrics/ClassLength
include CalculatedAdjustments
include PaymentMethodDistributors
@@ -11,9 +11,11 @@ module Spree
acts_as_paranoid
DISPLAY = [:both, :back_end].freeze
default_scope -> { where(deleted_at: nil) }
INTERNAL = Spree::PaymentMethod::CustomerCredit.to_s
default_scope -> { where(deleted_at: nil).where.not(type: INTERNAL) }
has_many :credit_cards, class_name: "Spree::CreditCard", dependent: :destroy
has_many :payments, class_name: "Spree::Payment", dependent: :restrict_with_error
validates :name, presence: true
validate :distributor_validation
@@ -52,6 +54,12 @@ module Spree
.where(environment: [Rails.env, "", nil])
}
# These method is used to get the internal payment method. It is accessible to all
# enterprise, but the accessibility is managed by the code, as opposed to using the database.
def self.customer_credit
unscoped.find_by(type: "Spree::PaymentMethod::CustomerCredit", deleted_at: nil)
end
def configured?
!stripe? || stripe_configured?
end
@@ -109,9 +117,23 @@ module Spree
distributors.include?(distributor)
end
def display_name
try_translating(name)
end
def display_description
try_translating(description)
end
def internal?
type == INTERNAL
end
private
def distributor_validation
return true if internal?
validates_with DistributorsValidator
end
@@ -126,5 +148,11 @@ module Spree
preferred_enterprise_id > 0 &&
stripe_account_id.present?
end
def try_translating(value)
return value if value.blank?
I18n.t(value, default: value)
end
end
end

View File

@@ -0,0 +1,110 @@
# frozen_string_literal: true
module Spree
class PaymentMethod
class CustomerCredit < Spree::PaymentMethod
# Name and description are translatable string, to allow instances to customise them
def name
"credit_payment_method.name"
end
def description
"credit_payment_method.description"
end
def actions
%w{internal_void}
end
# We should only void complete payment, otherwise we will be refunding credit that was
# not used in the first place.
def can_internal_void?(payment)
payment.state == "completed"
end
# Main method called by Spree::Payment::Processing during checkout
# - amount is in cents
# - options: {
# customer_id:, payment_id:, order_number:
# }
def purchase(amount, _source, options)
calculated_amount = amount / 100.00
customer = Customer.find_by(id: options[:customer_id])
return error_response("customer_not_found") if customer.nil?
return error_response("missing_payment") if options[:payment_id].nil?
available_credit = customer.customer_account_transactions.last&.balance
return error_response("no_credit_available") if available_credit.nil?
return error_response("not_enough_credit_available") if calculated_amount > available_credit
customer.with_lock do
description = I18n.t(
"order_payment_description",
scope: "credit_payment_method",
order_number: options[:order_number]
)
customer.customer_account_transactions.create(
amount: -calculated_amount,
currency:,
payment_id: options[:payment_id],
description:
)
end
message = I18n.t("success", scope: "credit_payment_method")
ActiveMerchant::Billing::Response.new(true, message)
end
# Main method called by Spree::Payment::Processing for void
# - amount is in cents
# - options: {
# customer_id:, payment_id:, order_number:, user_id: (optional)
# }
def void(amount, _source, options)
calculated_amount = amount / 100.00
customer = Customer.find_by(id: options[:customer_id])
return error_response("customer_not_found") if customer.nil?
return error_response("missing_payment") if options[:payment_id].nil?
customer.with_lock do
description = I18n.t(
"order_void_description",
scope: "credit_payment_method",
order_number: options[:order_number]
)
customer.customer_account_transactions.create(
amount: calculated_amount,
currency:,
payment_id: options[:payment_id],
description:,
created_by_id: options[:user_id]
)
end
message = I18n.t("void_success", scope: "credit_payment_method")
ActiveMerchant::Billing::Response.new(true, message)
end
def method_type
"check" # empty view
end
def source_required?
false
end
private
def error_response(translation_key)
message = I18n.t(translation_key, scope: "credit_payment_method.errors")
ActiveMerchant::Billing::Response.new(false, message)
end
def currency
CurrentConfig.get(:currency)
end
end
end
end

View File

@@ -23,6 +23,7 @@ module Spree
before_destroy :check_completed_orders
scope :admin, -> { where(admin: true) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
has_many :enterprise_roles, dependent: :destroy
has_many :enterprises, through: :enterprise_roles

View File

@@ -12,7 +12,8 @@ class CustomersWithBalanceQuery
joins(left_join_complete_orders).
group("customers.id").
select("customers.*").
select("#{outstanding_balance_sum} AS balance_value")
select("#{outstanding_balance_sum} AS balance_value").
select("#{available_credit} AS credit_value")
end
private
@@ -34,4 +35,21 @@ class CustomersWithBalanceQuery
def outstanding_balance_sum
"SUM(#{OutstandingBalanceQuery.new.statement})::float"
end
def available_credit
<<~SQL.squish
CASE WHEN EXISTS (#{available_credit_subquery}) THEN (#{available_credit_subquery})#{' '}
ELSE 0.00 END
SQL
end
def available_credit_subquery
<<~SQL.squish
SELECT balance
FROM customer_account_transactions
WHERE customer_account_transactions.customer_id = customers.id
ORDER BY id desc
LIMIT 1
SQL
end
end

View File

@@ -1,40 +0,0 @@
# frozen_string_literal: true
class InviteManagerReflex < ApplicationReflex
include ManagerInvitations
def invite
email = params[:email]
enterprise = Enterprise.find(params[:enterprise_id])
authorize! :edit, enterprise
existing_user = Spree::User.find_by(email:)
locals = { error: nil, success: nil, email:, enterprise: }
if existing_user
locals[:error] = I18n.t('admin.enterprises.invite_manager.user_already_exists')
return_morph(locals)
return
end
new_user = create_new_manager(email, enterprise)
if new_user.errors.empty?
locals[:success] = true
else
locals[:error] = new_user.errors.full_messages.to_sentence
end
return_morph(locals)
end
private
def return_morph(locals)
morph "#add_manager_modal",
render(partial: "admin/enterprises/form/add_new_unregistered_manager", locals:)
end
end

View File

@@ -7,9 +7,9 @@ module Api
# columns to instance methods. This way, the `balance_value` alias on that class ends up being
# `object.balance_value` here.
class CustomerWithBalanceSerializer < CustomerSerializer
attributes :balance, :balance_status
attributes :balance, :balance_status, :available_credit, :available_credit_url
delegate :balance_value, to: :object
delegate :balance_value, :credit_value, to: :object
def balance
Spree::Money.new(balance_value, currency: CurrentConfig.get(:currency)).to_s
@@ -24,6 +24,14 @@ module Api
""
end
end
def available_credit
Spree::Money.new(object.credit_value).to_s
end
def available_credit_url
admin_customer_customer_account_transaction_index_path(object.id)
end
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Api
module V1
class CustomerAccountTransactionSerializer < Api::V1::BaseSerializer
attributes :id, :customer_id, :amount, :currency, :description, :balance
end
end
end

View File

@@ -2,6 +2,6 @@
class Invoice
class PaymentMethodSerializer < ActiveModel::Serializer
attributes :id, :name, :description
attributes :id, :display_name, :display_description
end
end

View File

@@ -61,7 +61,7 @@ module Checkout
def set_payment_amount
return unless @order_params[:payments_attributes]
@order_params[:payments_attributes].first[:amount] = order.total
@order_params[:payments_attributes].first[:amount] = order.outstanding_balance.amount
end
def set_existing_card

View File

@@ -28,6 +28,8 @@ module Checkout
def validate_payment
return true if params.dig(:order, :payments_attributes, 0, :payment_method_id).present?
return true if order.zero_priced_order?
# No payment required, it's usually due to the order being paid by customer credit
return true if order.outstanding_balance <= 0.00
order.errors.add :payment_method, I18n.t('checkout.errors.select_a_payment_method')
end

View File

@@ -0,0 +1,28 @@
# frozen_string_literal: false
module CustomerAccountTransactions
class DataLoaderService
attr_reader :user, :enterprise
def initialize(user:, enterprise:)
@user = user
@enterprise = enterprise
end
def customer_account_transactions
return [] if user.customers.empty?
enterprise_customer = user.customers.find_by(enterprise: )
return [] if enterprise_customer.nil?
enterprise_customer.customer_account_transactions.order(id: :desc)
end
def available_credit
return 0 if customer_account_transactions.empty?
# We are ordered by newest, so the lastest transaction is the first one
customer_account_transactions.first.balance
end
end
end

View File

@@ -19,8 +19,9 @@ module Orders
end
def reset_other!(current_user, current_customer)
reset_user_and_customer(current_user)
reset_user(current_user)
reset_order_cycle(current_customer)
order.customer = current_customer
order.save!
end
@@ -28,7 +29,7 @@ module Orders
attr_reader :order, :distributor, :current_user
def reset_user_and_customer(current_user)
def reset_user(current_user)
return unless current_user
order.associate_user!(current_user) if order.user.blank? || order.email.blank?

View File

@@ -0,0 +1,115 @@
# frozen_string_literal: true
module Orders
class CustomerCreditService
def initialize(order)
@order = order
end
def apply
add_payment_with_credit if credit_available?
end
def refund(user: nil) # rubocop:disable Metrics/AbcSize
if order.payment_state != "credit_owed"
return Response.new(
success: false, message: I18n.t(:no_credit_owed, scope: translation_scope)
)
end
if credit_payment_method.nil?
error_message = I18n.t(:credit_payment_method_missing, scope: translation_scope)
log_error(error_message)
return Response.new(success: false, message: error_message)
end
amount = order.new_outstanding_balance
order.customer.with_lock do
payment = order.payments.create!( payment_method: credit_payment_method, amount: amount,
state: "completed", skip_source_validation: true)
options = { customer_id: order.customer_id, payment_id: payment.id,
order_number: order.number, user_id: user&.id }
response = credit_payment_method.void((-1 * amount * 100).round, nil, options)
raise response.message if response.failure?
Response.new(success: true, message: I18n.t(:refund_sucessful, scope: translation_scope))
end
rescue StandardError => e
# Even though the transaction rolled back, the order still have a payment in memory,
# so we reload the payments so the payment doesn't get saved later on
order.payments.reload
log_error(e)
Response.new(success: false, message: e.to_s)
end
private
attr_reader :order
def add_payment_with_credit
if credit_payment_method.nil?
error_message = I18n.t(:credit_payment_method_missing, scope: translation_scope)
log_error(error_message)
return
end
return if order.payments.where(payment_method: credit_payment_method).exists?
# we are already in a transaction because the order is locked, so we force creating a new one
# to make sure the rollback works as expected :
# https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions
ActiveRecord::Base.transaction(requires_new: true) do
amount = [available_credit, order.total].min
payment = order.payments.create!(payment_method: credit_payment_method, amount:)
payment.internal_purchase!
end
rescue StandardError => e
# Even though the transaction rolled back, the order still have a payment in memory,
# so we reload the payments so the payment doesn't get saved later on
order.payments.reload
log_error(e)
end
def credit_available?
return false if order.customer.nil?
available_credit > 0
end
def available_credit
@available_credit ||= order.customer.customer_account_transactions.last&.balance || 0.00
end
def credit_payment_method
Spree::PaymentMethod.customer_credit
end
def log_error(error)
Rails.logger.error("Orders::CustomerCreditService: #{error}")
Alert.raise(error)
end
def translation_scope
"orders.customer_credit_service"
end
class Response
attr_reader :message
def initialize(success:, message:)
@success = success
@message = message
end
def success?
@success
end
def failure?
!success?
end
end
end
end

View File

@@ -40,7 +40,7 @@ module PermittedAttributes
:hide_ofn_navigation, :white_label_logo, :white_label_logo_link,
:hide_groups_tab, :external_billing_id,
:enable_producers_to_edit_orders,
:remove_logo, :remove_promo_image, :remove_white_label_logo
:remove_logo, :remove_promo_image, :remove_white_label_logo, :contact_id
]
end
end

View File

@@ -0,0 +1,31 @@
= turbo_stream.update "customer-account-transactions-modal-container" do
= render ModalComponent.new(id: "customer-account-transactions-modal", instant: true, modal_class: "big") do
%h3
= t(".available_credit", available_credit: Spree::Money.new(@available_credit))
%table.index
%thead
%tr
%th.transaction-date
= t(".transaction_date")
%th.description
= t(".description")
%th.created-by
= t(".created_by")
%th.amount
= t(".amount")
%th.running-balance
= t(".running_balance")
%tbody
- @collection.each do |transaction|
%tr.transaction
%td.transaction-date
= transaction.updated_at.strftime("%Y-%m-%d")
%td.description
= transaction.description
%td.created-by
= transaction.created_by&.email
%td.amount
= Spree::Money.new(transaction.amount)
%td.running-balance
= Spree::Money.new(transaction.balance)

View File

@@ -48,6 +48,7 @@
%input.red{ type: "button", value: t(:save_changes), "ng-click": "submitAll(customers_form)", "ng-disabled": "!hasUnsavedChanges()" }
%table.index#customers{ 'ng-show' => '!RequestMonitor.loading && filteredCustomers.length > 0' }
%col.id{ width: "5%", 'ng-show' => 'columns.id.visible' }
%col.email{ width: "20%", 'ng-show' => 'columns.email.visible' }
%col.first_name{ width: "20%", 'ng-show' => 'columns.first_name.visible' }
%col.last_name{ width: "20%", 'ng-show' => 'columns.last_name.visible' }
@@ -61,6 +62,7 @@
%tr{ "ng-controller": "ColumnsCtrl" }
-# %th.bulk
-# %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" }
%th.id{ 'ng-show' => 'columns.id.visible' }=t('admin.customers.index.id')
%th.email{ 'ng-show' => 'columns.email.visible' }
%a{ :href => '', 'ng-click' => "sorting.toggle('email')" }=t('admin.email')
%th.first_name{ 'ng-show' => 'columns.first_name.visible' }
@@ -73,10 +75,13 @@
%th.bill_address{ 'ng-show' => 'columns.bill_address.visible' }=t('admin.customers.index.bill_address')
%th.ship_address{ 'ng-show' => 'columns.ship_address.visible' }=t('admin.customers.index.ship_address')
%th.balance{ 'ng-show' => 'columns.balance.visible' }=t('admin.customers.index.balance')
%th.credit{ 'ng-show' => 'columns.credit.visible' }=t('admin.customers.index.credit')
%tbody
%tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy: sorting.predicate:sorting.reverse ) | limitTo:customerLimit track by customer.id", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" }
-# %td.bulk
-# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' }
%td.id{ 'ng-show' => 'columns.id.visible'}
%span{ 'ng-bind' => '::customer.id' }
%td.email{ 'ng-show' => 'columns.email.visible'}
%span{ 'ng-bind' => '::customer.email' }
%span.guest-label{ 'ng-show' => 'customer.user_id == null' }= t('.guest_label')
@@ -98,9 +103,14 @@
%td.balance.align-center{ 'ng-show' => 'columns.balance.visible'}
%span.state.white-space-nowrap{ 'ng-class' => 'customer.balance_status', 'ng-bind' => 'displayBalanceStatus(customer)' }
%span{ 'ng-bind' => '::customer.balance' }
%td.balance.align-center{ 'ng-show' => 'columns.credit.visible', "data-turbo": true}
%a{ "ng-href": "{{customer.available_credit_url}}", "data-turbo-stream": "" }
%span{ 'ng-bind' => '::customer.available_credit' }
%td.actions
%a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" }
.text-center{ "ng-show": "filteredCustomers.length > customerLimit" }
%input{ type: 'button', value: t(:show_more), "ng-click": 'customerLimit = customerLimit + 20' }
%input{ type: 'button', value: t(:show_all_with_more, num: '{{ filteredCustomers.length - customerLimit }}'), "ng-click": 'customerLimit = filteredCustomers.length' }
#customer-account-transactions-modal-container

View File

@@ -1,21 +0,0 @@
%form#add_manager_modal{ 'data-reflex': 'submit->InviteManager#invite', 'data-reflex-serialize-form': true }
.margin-bottom-30.text-center
.text-big
= t('js.admin.modals.invite_title')
- if success
%p.alert-box.ok= t('user_invited', email: email)
- if error
%p.alert-box.error= error
= text_field_tag :email, nil, class: 'fullwidth margin-bottom-20'
= hidden_field_tag :enterprise_id, @enterprise&.id || enterprise.id
.modal-actions
- if success
%input{ class: "button icon-plus secondary", type: 'button', value: t('js.admin.modals.close'), "data-action": "click->help-modal#close" }
- else
%input{ class: "button icon-plus secondary", type: 'button', value: t('js.admin.modals.cancel'), "data-action": "click->help-modal#close" }
= submit_tag "#{t('js.admin.modals.invite')}"

View File

@@ -11,7 +11,7 @@
%tbody
- @payment_methods.each do |payment_method|
%tr
%td= payment_method.name
%td= payment_method.display_name
%td= f.check_box :payment_method_ids, { multiple: true }, payment_method.id, nil
%td= link_to t(:edit), edit_admin_payment_method_path(payment_method)
%br

View File

@@ -1,75 +1,54 @@
- owner_email = @enterprise&.owner&.email || ""
- full_permissions = (spree_current_user.admin? || spree_current_user == @enterprise&.owner)
.row
.three.columns.alpha
=f.label :owner_id, t('.owner')
- if full_permissions
%span.required *
= render partial: 'admin/shared/whats_this_tooltip', locals: {tooltip_text: t('.owner_tip')}
.eight.columns.omega
- if full_permissions
= f.hidden_field :owner_id, class: "select2 fullwidth", 'user-select' => 'Enterprise.owner', 'ng-model' => 'Enterprise.owner'
- else
= owner_email
= t '.description'
.row
.three.columns.alpha
=f.label :user_ids, t('.notifications')
- if full_permissions
%span.required *
= render partial: 'admin/shared/whats_this_tooltip', locals: {tooltip_text: t('.contact_tip')}
.eight.columns.omega
- if full_permissions
%select.select2.fullwidth{ id: 'receives_notifications_dropdown', name: 'receives_notifications', "ng-model": 'receivesNotifications', "ng-init": "receivesNotifications = '#{@enterprise.contact.id}'" }
%option{ value: '{{user.id}}', "ng-repeat": 'user in Enterprise.users', "ng-selected": "user.id == #{@enterprise.contact.id}", "ng-hide": '!user.confirmed' }
{{user.email}}
- else
= @enterprise.contact.email
- if full_permissions && @enterprise.users.count > 0
- enterprise_role_ids_by_user_id = @enterprise.enterprise_roles.pluck(:user_id, :id).to_h
.row
.three.columns.alpha
=f.label :user_ids, t('.managers')
- if full_permissions
%span.required *
= render partial: 'admin/shared/whats_this_tooltip', locals: {tooltip_text: t('.managers_tip')}
.eight.columns.omega
- if full_permissions
%table.managers
%tr
%table.managers
%thead
%tr
%th= t('.manager')
%th.center
= t('.owner')
= render AdminTooltipComponent.new(text: t('.owner_tip'), link_text: %[<i class="fa fa-question-circle"></i>].html_safe, link: nil)
%th.center
= t('.contact')
= render AdminTooltipComponent.new(text: t('.contact_tip'), link_text: %[<i class="fa fa-question-circle"></i>].html_safe, link: nil)
%tbody
- @enterprise.users.each do |user|
- contact = user.id == @enterprise.contact&.id
- owner = user.id == @enterprise.owner&.id
%tr{ id: "manager-#{user.id}" }
%td
- # Ignore this input in the submit
= hidden_field_tag :ignored, nil, class: "select2 fullwidth", 'user-select' => 'newManager', 'ng-model' => 'newManager'
= user.email
- if user.confirmed?
%i.confirmation.confirmed.fa.fa-check-circle{ "ofn-with-tip": t('.email_confirmed') }
- else
%i.confirmation.unconfirmed.fa.fa-exclamation-triangle{ "ofn-with-tip": t('.email_not_confirmed') }
%td.center
- if user.confirmed?
= f.label :owner_id, t(".set_as_owner", email: user.email), class: "sr-only", value: user.id
= f.radio_button :owner_id, user.id
%td.center
- if user.confirmed?
= f.label :owner_id, t(".set_as_contact", email: user.email), class: "sr-only", value: user.id
= f.radio_button :contact_id, user.id
%td.actions
%tr.animate-repeat{ id: "manager-{{manager.id}}", "ng-repeat": 'manager in Enterprise.users' }
%td
= hidden_field_tag "enterprise[user_ids][]", nil, multiple: true, 'ng-value' => 'manager.id'
{{ manager.email }}
%i.confirmation.confirmed.fa.fa-check-circle{ "ofn-with-tip": t('.email_confirmed'), "ng-show": 'manager.confirmed' }
%i.confirmation.unconfirmed.fa.fa-exclamation-triangle{ "ofn-with-tip": t('.email_not_confirmed'), "ng-show": '!manager.confirmed' }
%i.role.contact.fa.fa-envelope-o{ "ofn-with-tip": t('.contact'), "ng-show": 'manager.id == receivesNotifications' }
%i.role.owner.fa.fa-star{ "ofn-with-tip": t('.owner'), "ng-show": 'manager.id == Enterprise.owner.id' }
%td.actions
%a{ class: "icon-trash no-text", "ng-click": 'removeManager(manager)', "ng-class": "{disabled: manager.id == Enterprise.owner.id || manager.id == receivesNotifications}" }
- if !owner && !contact
= link_to_delete user, no_text: true, url: admin_enterprise_role_path(id: enterprise_role_ids_by_user_id[user.id])
- else
%a{ class: "icon-trash no-text disabled" }
- else
- @enterprise.users.each do |manager|
= manager.email
%br
%a.button{ href: "#{new_admin_enterprise_user_invitation_path(@enterprise)}", data: { turbo_stream: true, turbo: true } }
%i.icon-plus
= t('.invite_manager')
%br
- if full_permissions
%form
.row
.three.columns.alpha
%label
= t('.invite_manager')
= render partial: 'admin/shared/whats_this_tooltip', locals: {tooltip_text: t('.invite_manager_tip')}
.eight.columns.omega
.row
%a.button{ "data-controller": "help-modal-link", "data-action": "click->help-modal-link#open", "data-help-modal-link-target-value": "invite-manager-modal" }
= t('.add_unregistered_user')
-# add to admin footer to avoid nesting invitation form inside enterprise form
- content_for :admin_footer do
= render HelpModalComponent.new(id: "invite-manager-modal", close_button: false) do
= render partial: 'admin/enterprises/form/add_new_unregistered_manager', locals: { error: nil, success: nil }
- else
- @enterprise.users.each do |manager|
= manager.email
%br

View File

@@ -68,11 +68,11 @@
@order_cycle.distributor_payment_methods.include?(distributor_payment_method),
id: "order_cycle_selected_distributor_payment_method_ids_#{distributor_payment_method.id}",
data: ({ "checked-target" => "checkbox" } if distributor_payment_method.payment_method.frontend?)
= distributor_payment_method.payment_method.name
= distributor_payment_method.payment_method.display_name
- distributor.payment_methods.inactive_or_backend.each do |payment_method|
%label.disabled
= check_box_tag nil, nil, false, disabled: true
= payment_method.name
= payment_method.display_name
= "(#{t('.back_end')})"
- if distributor.payment_methods.available.none?
%p

View File

@@ -1,24 +1,24 @@
- link = pagy_anchor(pagy)
.pagination{ "data-controller": "search" }
%nav.pagination{ "aria-label": t('.navigation'), "data-controller": "search" }
- if pagy.prev
%button.page.prev{ data: { action: 'click->search#changePage', page: pagy.prev } }
%a.page.prev{ href: "#", rel: "prev", "aria-label": t('.previous'), data: { action: 'click->search#changePage', page: pagy.prev } }
%i.icon-chevron-left{ data: { action: 'click->search#changePage', page: pagy.prev } }
- else
%button.page.disabled{disabled: "disabled"}!= pagy_t('pagy.prev')
- pagy.series.each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
- if item.is_a?(Integer) # page link
%button.page{ data: { action: 'click->search#changePage', page: item } }= item
%ul.pagelist
- pagy.series.each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
- if item.is_a?(Integer) # page link
%li
%a.page{ href: "#", "aria-label": t('.page', number: item), data: { action: 'click->search#changePage', page: item } }= item
- elsif item.is_a?(String) # current page
%button.page.current.active= item
- elsif item.is_a?(String) # current page
%li
%a.page.current.active{ href: "#", "aria-label": t('.page', number: item), "aria-current": "page" }= item
- elsif item == :gap # page gap
%span.page.gap.pagination-ellipsis!= pagy_t('pagy.gap')
- elsif item == :gap # page gap
%li
%span.page.gap.pagination-ellipsis!= pagy_t('pagy.gap')
- if pagy.next
%button.page.next{ data: { action: 'click->search#changePage', page: pagy.next } }
%a.page.next{ href: "#", rel: "next", "aria-label": t('.next'), data: { action: 'click->search#changePage', page: pagy.next } }
%i.icon-chevron-right{ data: { action: 'click->search#changePage', page: pagy.next } }
- else
%button.page.disabled.pagination-next{disabled: "disabled"}!= pagy_t('pagy.next')

View File

@@ -0,0 +1,5 @@
= turbo_stream.update "remote_modal", ""
= turbo_stream.update "users_panel" do
= render partial: "admin/enterprises/form/users", locals: { f: ActionView::Helpers::FormBuilder.new(:enterprise, @enterprise, self, {}) }
= turbo_stream.append "flashes" do
= render partial: 'admin/shared/flashes', locals: { flashes: flash }

View File

@@ -0,0 +1,17 @@
= turbo_stream.update "remote_modal" do
= render ModalComponent.new id: "#modal_new_user_invitation", instant: true, close_button: false, modal_class: :fit do
= form_with model: @user_invitation, url: admin_enterprise_user_invitations_path(@enterprise), html: { name: "user_invitation", data: { turbo: true } } do |f|
%h2= t ".invite_new_user"
%p= t ".description"
%fieldset.no-border-top.no-border-bottom
.row
= f.label :email, t(:email)
= f.email_field :email, placeholder: t('.eg_email_address'), data: { controller: "select-user" }, inputmode: "email", autocomplete: "off"
= f.error_message_on :email
.modal-actions.justify-end.filter-actions
%input{ class: "secondary relaxed", type: 'button', value: t('.back'), "data-action": "click->modal#close" }
%button.button.primary.relaxed.icon-envelope{ type: "submit" }
= t(".invite")

View File

@@ -8,10 +8,17 @@
.checkout-title
= t("checkout.step2.payment_method.title")
- if @order.zero_priced_order?
- if @order.zero_priced_order? || @order.outstanding_balance.zero?
%h3= t(:no_payment_required)
= hidden_field_tag "order[payments_attributes][][amount]", 0
- if @order.zero_priced_order?
= hidden_field_tag "order[payments_attributes][][amount]", 0
- if @paid_with_credit
= t(:credit_used, amount: Spree::Money.new(@paid_with_credit))
- else
- if @paid_with_credit
= t(:credit_used, amount: Spree::Money.new(@paid_with_credit))
- selected_payment_method = @order.payments&.with_state(:checkout)&.first&.payment_method_id
- selected_payment_method ||= available_payment_methods[0].id if available_payment_methods.length == 1
- available_payment_methods.each do |payment_method|
@@ -23,13 +30,13 @@
"data-action": "paymentmethod#selectPaymentMethod",
"data-paymentmethod-id": "#{payment_method.id}",
"data-paymentmethod-target": "input"
= f.label :payment_method_id, "#{payment_method.name}", for: "payment_method_#{payment_method.id}"
= f.label :payment_method_id, "#{payment_method.display_name}", for: "payment_method_#{payment_method.id}"
%em.fees=payment_or_shipping_price(payment_method, @order)
.paymentmethod-container{"data-paymentmethod-id": "#{payment_method.id}", style: "display: #{payment_method.id == selected_payment_method ? "block" : "none"}"}
- if payment_method.description && !payment_method.description.empty?
.paymentmethod-description.panel
= simple_format(html_escape(payment_method.description))
= simple_format(html_escape(payment_method.display_description))
.paymentmethod-form
= render partial: "checkout/payment/#{payment_method.method_type}", locals: { payment_method: payment_method, f: f }

View File

@@ -30,7 +30,7 @@
- payment_method = last_payment_method(@order)
%div
- if payment_method
= payment_method.name
= payment_method.display_name
%em.fees
= payment_or_shipping_price(payment_method, @order)
- elsif @order.zero_priced_order?
@@ -41,7 +41,7 @@
.summary-subtitle
= t("checkout.step3.payment_method.instructions")
%div
= payment_method&.description
= payment_method&.display_description
.checkout-substep
@@ -56,7 +56,8 @@
.summary-right{ "data-controller": "sticky", "data-sticky-target": "container" }
.summary-right-line.total
.summary-right-line-label= t :order_total_price
.summary-right-line-value#order_total= @order.display_total.to_html
.summary-right-line-value#order_total= @order.display_outstanding_balance.to_html
.summary-right-line
.summary-right-line-label= t :order_produce
@@ -78,6 +79,11 @@
.summary-right-line-label= t :order_includes_tax
.summary-right-line-value#tax-row= display_checkout_tax_total(@order)
- if @paid_with_credit.present?
.summary-right-line
.summary-right-line-label= t :customer_credit
.summary-right-line-value#customer-credit= Spree::Money.new(-1 * @paid_with_credit).to_html
.checkout-submit
- if any_terms_required?(@order.distributor)
= render partial: "terms_and_conditions", locals: { f: f }

View File

@@ -6,21 +6,21 @@
%p
= t :brandstory_intro
#brand-story-text.hide-show.slideable
%p
= t :brandstory_part1
%p
= t :brandstory_part2
%p
= t :brandstory_part3
%p
= t :brandstory_part4
%p
%strong
= t :brandstory_part5_strong
%p
= t :brandstory_part6
%a.text-vbig{"slide-toggle" => "#brand-story-text", "ng-click" => "toggleBrandStory()"}
%i.ofn-i_005-caret-down{"ng-hide" => "brandStoryExpanded"}
%i.ofn-i_006-caret-up{ "ng-show" => "brandStoryExpanded"}
%details#brand-story-text
%summary
%i.ofn-i_005-caret-down
%i.ofn-i_006-caret-up
.brand-story-content
%p
= t :brandstory_part1
%p
= t :brandstory_part2
%p
= t :brandstory_part3
%p
= t :brandstory_part4
%p
%strong
= t :brandstory_part5_strong
%p
= t :brandstory_part6

View File

@@ -5,7 +5,7 @@
- content_for :page_alert do
= render "shared/menu/alert"
%div{"ng-controller" => "HomeCtrl"}
%div
= render "home/tagline"
#panes

View File

@@ -1,6 +0,0 @@
%label{for: path}= name
%input.medium.input-text{attributes}
%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"}
= "{{ fieldErrors('#{path}') }}"

View File

@@ -1,6 +0,0 @@
%label{for: path}= name
= select_tag path, options_for_select(options), attributes
%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"}
= "{{ fieldErrors('#{path}') }}"

View File

@@ -17,4 +17,4 @@
%p.callout{style: "margin-top: 40px"}
%strong
= t :email_payment_description
%p{style: "margin: 5px"}= @order.last_payment_method.description
%p{style: "margin: 5px"}= @order.last_payment_method.display_description

View File

@@ -4,7 +4,7 @@
- content_for :page_title do
= t('.editing_payment_method')
%i.icon-arrow-right
= @payment_method.name
= @payment_method.display_name
- content_for :page_actions do
%li
= button_link_to t('.new'), spree.new_admin_payment_method_path, icon: 'icon-plus'

View File

@@ -32,7 +32,7 @@
%tbody
- @payment_methods.each do |method|
%tr{class: "#{cycle('odd', 'even')}", id: "#{spree_dom_id method}"}
%td.align-center= method.name
%td.align-center= method.display_name
%td.align-center
- method.distributors.each do |distributor|
= distributor.name

View File

@@ -14,7 +14,7 @@
%li
%label
= radio_button_tag 'payment[payment_method_id]', method.id, method == @payment_method, { class: "payment_methods_radios", "ng-model" => 'form_data.payment_method' }
= t(method.name, scope: :payment_methods, default: method.name)
= method.display_name
.payment-method-settings
- @payment_methods.each do |method|
.payment-methods{id: "payment_method_#{method.id}"}

View File

@@ -2,6 +2,10 @@
= render partial: 'spree/admin/shared/order_tabs', locals: { current: 'Payments' }
- content_for :page_actions do
- if @order.payment_state == "credit_owed"
%li#credit_customer_section
= button_link_to t(:credit_customer), admin_order_payments_credit_customer_url(@order), data: { method: :post }
- if @order.outstanding_balance?
%li#new_payment_section
= button_link_to t(:new_payment), new_admin_order_payment_url(@order), icon: 'icon-plus'

View File

@@ -14,7 +14,7 @@
= " - OFN #{t(:administration)}"
%link{:href => "https://fonts.googleapis.com/css?family=Open+Sans:400italic,600italic,400,600&subset=latin,cyrillic,greek,vietnamese", :rel => "stylesheet", :type => "text/css"}
= stylesheet_pack_tag 'admin-style-v3', media: "screen, print"
= stylesheet_pack_tag 'admin-style', media: "screen, print"
= render "layouts/bugsnag_js"
- if content_for? :minimal_js

View File

@@ -4,25 +4,21 @@
#new_variant
%table.index.sortable{"data-sortable-link" => update_positions_admin_product_variants_path(@product)}
%table.index
%colgroup
%col{style: "width: 5%"}/
%col{style: "width: 25%"}/
%col{style: "width: 20%"}/
%col{style: "width: 20%"}/
%col{style: "width: 15%"}/
%col{style: "width: 15%"}/
%col{style: "width: 25%"}/
%col{style: "width: 25%"}/
%col{style: "width: 25%"}/
%thead
%tr
%th{colspan: "2"}= t('.options')
%th= t('.price')
%th= t('.sku')
%th= t('.options')
%th{ style: 'text-align: center;' }= t('.price')
%th{ style: 'text-align: center;' }= t('.sku')
%th.actions
%tbody
- @variants.each do |variant|
%tr{id: spree_dom_id(variant), class: cycle('odd', 'even'), style: "#{"color:red;" if variant.deleted? }" }
%td.no-border
%span.handle
%td= variant.full_name
%td.align-center= variant.display_price.to_html
%td.align-center= variant.sku

View File

@@ -60,10 +60,12 @@
= yield :sidebar
= render "admin/terms_of_service_banner" if tos_need_accepting?
%script
= raw "Spree.api_key = \"#{spree_current_user.try(:spree_api_key).to_s}\";"
= render "layouts/matomo_tag"
= yield :admin_footer
#remote_modal

View File

@@ -48,9 +48,17 @@
%h5
= t :orders_form_total
%td.text-right
%h5.order-total.grand-total= @order.display_total
%h5.order-total.grand-total= @order.display_total.to_html
%td
- if @paid_with_credit.present?
%tr
%td.text-right{colspan: "3"}
%strong
= t :customer_credit
%td.text-right#customer-credit
%span.order-total= Spree::Money.new(-1 * @paid_with_credit).to_html
- if @order.total_tax > 0
%tr
%td.text-right{colspan:"3"}

View File

@@ -25,20 +25,28 @@
= t :order_total_price
%td.text-right.total
%h5#order_total= order.display_total.to_html
- if @paid_with_credit.present?
%tr.total
%td.text-right{colspan: "3"}
%strong
= t :customer_credit
%td.text-right.total#customer-credit
%strong
= Spree::Money.new(-1 * @paid_with_credit).to_html
%tr.total
%td.text-right{colspan: "3"}
%strong
= t :order_amount_paid
%td.text-right.total{id: "amount-paid"}
%td.text-right.total#amount-paid
%strong
= order.display_payment_total.to_html
= Spree::Money.new(@payment_total).to_html
- if order.outstanding_balance.positive?
%tr.total
%td.text-right{colspan: "3"}
%h5.not-paid
= t :order_balance_due
%td.text-right.total.not-paid
%h5.not-paid
%h5.not-paid#balance-due
= order.display_outstanding_balance.to_html
- if order.outstanding_balance.negative?
%tr.total

View File

@@ -15,9 +15,9 @@
- if (order_payment_method = last_payment_method(order))
.text-big
= t :order_payment
%strong= order_payment_method&.name
%strong= order_payment_method&.display_name
%p.text-small.text-skinny.pre-line.word-wrap
%em= order_payment_method&.description
%em= order_payment_method&.display_description
- else
.text-big
= t(:no_payment_required)

View File

@@ -17,4 +17,4 @@
%p.callout{style: "margin-top: 40px"}
%strong
= t :email_payment_description
%p{style: "margin: 5px"}= last_payment_method(@order).description
%p{style: "margin: 5px"}= last_payment_method(@order).display_description

View File

@@ -0,0 +1,41 @@
%script{ type: "text/ng-template", id: "account/customer_account_transactions.html" }
.active_table.orders
%h3= t('.title')
- @shops.each do |shop|
- data_loader = CustomerAccountTransactions::DataLoaderService.new(user: @user, enterprise: shop)
%distributor.active_table_node.row.animate-repeat.closed.inactive{ "data-controller": "frontend-toggle-control", "data-frontend-toggle-control-selector-value": "#transaction-list-#{shop.id}", "data-frontend-toggle-control-target": "classUpdate" }
.small-12.columns
.row.active_table_row.skinny-head.margin-top.closed{ "data-action": "click->frontend-toggle-control#toggleDisplay", "data-frontend-toggle-control-target": "classUpdate" }
.columns.small-2
- if shop.logo_url(:medium)
= image_tag shop.logo_url(:medium), class: "margin-top account-logo"
- else
%i.ofn-i_059-producer
.columns.small-5
%h3.margin-top
= link_to shop.name, enterprise_url_selector(shop)
.columns.small-4.text-right
%h3.margin-top.distributor-balance
= t(".credit_available", credit: Spree::Money.new(data_loader.available_credit))
.columns.small-1.text-right
%h3.margin-top
%i.ofn-i_005-caret-down{ "data-frontend-toggle-control-target": "chevron" }
.row{ style: "display: none", id: "transaction-list-#{shop.id}" }
.columns.small-12.fat
%table
%tr
%th.order1= t(".transaction_date")
%th.order2= t(".description")
%th.order4= t(".amount")
%th.order5= t(".running_balance")
%tbody.transaction-group
- data_loader.customer_account_transactions.each do |transaction|
%tr.transaction-row.even
%td.order1
= transaction.updated_at.strftime("%Y-%m-%d")
%td.order2
= transaction.description
%td.order4
= Spree::Money.new(transaction.amount)
%td.order5
= Spree::Money.new(transaction.balance)

View File

@@ -19,18 +19,21 @@
= render 'orders'
= render 'cards'
= render 'transactions'
= render 'customer_account_transactions'
= render 'settings'
= render 'developer_settings' if @user.show_api_key_view
.row.tabset-ctrl#account-tabs{ style: 'margin-bottom: 100px', navigate: 'true', selected: 'orders', prefix: 'account' }
.small.12.medium-2.columns.tab{ name: "orders" }
.small.12.medium-3.columns.tab{ name: "orders" }
%a=t('.tabs.orders')
- if Spree::Config.stripe_connect_enabled && Stripe.publishable_key
.small.12.medium-2.columns.tab{ name: "cards" }
.small.12.medium-3.columns.tab{ name: "cards" }
%a=t('.tabs.cards')
.small.12.medium-2.columns.tab{ name: "transactions" }
.small.12.medium-3.columns.tab{ name: "transactions" }
%a=t('.tabs.transactions')
.small.12.medium-2.columns.tab{ name: "settings" }
.small.12.medium-3.columns.tab{ name: "customer_account_transactions" }
%a=t('.tabs.customer_account_transactions')
.small.12.medium-3.columns.tab{ name: "settings" }
%a=t('.tabs.settings')
// the api_keys partial is the only content for now, so we have to hide the whole tab for now
// if there is new content, we will need to handle this inside the developer_settings partial

View File

@@ -0,0 +1,23 @@
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ["chevron", "classUpdate"];
static values = { selector: String };
toggleDisplay(_event) {
if (this.hasChevronTarget) {
this.chevronTarget.classList.toggle("ofn-i_005-caret-down");
this.chevronTarget.classList.toggle("ofn-i_006-caret-up");
}
if (this.hasClassUpdateTarget) {
this.classUpdateTargets.forEach((t) => {
t.classList.toggle("closed");
t.classList.toggle("open");
});
}
const element = document.querySelector(this.selectorValue);
element.style.display = element.style.display === "none" ? "block" : "none";
}
}

View File

@@ -0,0 +1,44 @@
import { Controller } from "stimulus";
import TomSelect from "tom-select/dist/esm/tom-select.complete";
import showHttpError from "js/services/show_http_error";
export default class extends Controller {
connect() {
this.control = new TomSelect(this.element, {
create: true,
plugins: ["dropdown_input"],
labelField: "email",
load: this.#load.bind(this),
maxItems: 1,
persist: false,
searchField: ["email"],
shouldLoad: (query) => query.length > 2,
valueField: "email",
});
}
disconnect() {
if (this.control) this.control.destroy();
}
// private
#load(query, callback) {
const url = "/admin/search/known_users.json?q=" + encodeURIComponent(query);
fetch(url)
.then((response) => {
if (!response.ok) {
showHttpError(response.status);
throw response;
}
return response.json();
})
.then((json) => {
callback({ items: json });
})
.catch((error) => {
console.log(error);
callback();
});
}
}

View File

@@ -16,13 +16,13 @@
@import "flatpickr/dist/themes/material_blue";
@import "shortcut-buttons-flatpickr/dist/themes/light";
@import "../admin/globals/functions";
@import "globals/functions";
@import "globals/palette"; // admin_v3
@import "globals/variables"; // admin_v3
@import "../admin/globals/mixins";
@import "globals/mixins";
@import "mixins"; // admin_v3
@import "../admin/plugins/font-awesome";
@import "plugins/font-awesome";
@import "../shared/variables/layout";
@import "../shared/variables/variables";
@@ -32,7 +32,7 @@
@import "shared/icons"; // admin_v3
@import "shared/forms"; // admin_v3
@import "shared/layout"; // admin_v3
@import "../admin/shared/scroll_bar";
@import "shared/scroll_bar";
@import "../shared/trix";
@@ -40,96 +40,94 @@
@import "plugins/powertip"; // admin_v3
@import "sections/orders"; // admin_v3
@import "../admin/sections/products";
@import "sections/products";
@import "../admin/hacks/mozilla";
@import "../admin/hacks/opera";
@import "../admin/hacks/ie";
@import "hacks/mozilla";
@import "hacks/opera";
@import "hacks/ie";
@import "components/actions"; // admin_v3
@import "../admin/components/alert-box";
@import "../admin/components/alert_row";
@import "components/alert-box";
@import "components/alert_row";
@import "components/buttons"; // admin_v3
@import "components/date-picker"; // admin_v3
@import "../admin/components/dialogs";
@import "../admin/components/input";
@import "../admin/components/jquery_dialog";
@import "components/dialogs";
@import "components/input";
@import "components/jquery_dialog";
@import "components/messages"; // admin_v3
@import "components/navigation"; // admin_v3
@import "../admin/components/ng-cloak";
@import "../admin/components/page_actions";
@import "components/ng-cloak";
@import "components/page_actions";
@import "components/pagination"; // admin_v3
@import "../admin/components/per_page_controls";
@import "../admin/components/product_autocomplete";
@import "../admin/components/progress";
@import "../admin/components/save_bar";
@import "components/per_page_controls";
@import "components/product_autocomplete";
@import "components/progress";
@import "components/save_bar";
@import "components/sidebar"; // admin_v3
@import "../admin/components/simple_modal";
@import "../admin/components/states";
@import "../admin/components/stripe_connect_button";
@import "../admin/components/subscriptions_states";
@import "../admin/components/table-filter";
@import "../admin/components/table_loading";
@import "../admin/components/timepicker";
@import "../admin/components/todo";
@import "../admin/components/tooltip";
@import "../admin/components/wizard_progress";
@import "components/simple_modal";
@import "components/states";
@import "components/stripe_connect_button";
@import "components/subscriptions_states";
@import "components/table-filter";
@import "components/table_loading";
@import "components/timepicker";
@import "components/todo";
@import "components/tooltip";
@import "components/wizard_progress";
@import "../admin/pages/enterprise_form";
@import "../admin/pages/subscription_form";
@import "../admin/pages/subscription_line_items";
@import "../admin/pages/subscription_review";
@import "pages/enterprise_form";
@import "pages/subscription_form";
@import "pages/subscription_line_items";
@import "pages/subscription_review";
@import "../admin/advanced_settings";
@import "../admin/alert";
@import "../admin/animations";
@import "advanced_settings";
@import "alert";
@import "animations";
@import "pages/change_type_form"; // admin_v3
@import "../admin/connected_apps";
@import "../admin/customers";
@import "connected_apps";
@import "customers";
@import "dashboard/dashboard_item"; // admin_v3
@import "pages/dashboard-single-ent"; // admin_v3
@import "../admin/dialog";
@import "../admin/disabled";
@import "dialog";
@import "disabled";
@import "components/dropdown"; // admin_v3
@import "pages/edit_variant"; // admin_v3
@import "pages/enterprise_index_panels"; // admin_v3
@import "../admin/enterprises";
@import "../admin/filters_and_controls";
@import "../admin/grid";
@import "../admin/icons";
@import "../admin/index_panel_buttons";
@import "../admin/index_panels";
@import "../admin/modals";
@import "../admin/offsets";
@import "../admin/openfoodnetwork";
@import "../admin/order_cycles";
@import "../admin/orders";
@import "enterprises";
@import "filters_and_controls";
@import "grid";
@import "icons";
@import "index_panel_buttons";
@import "index_panels";
@import "modals";
@import "offsets";
@import "openfoodnetwork";
@import "order_cycles";
@import "orders";
@import "pages/product_import"; // admin_v3
@import "../admin/products";
@import "../admin/products_v3";
@import "../admin/question-mark-tooltip";
@import "../admin/relationships";
@import "../admin/reports";
@import "products";
@import "products_v3";
@import "question-mark-tooltip";
@import "relationships";
@import "reports";
@import "components/select2"; // admin_v3
@import "components/sidebar-item"; // admin_v3
@import "../admin/side_menu";
@import "../admin/tables";
@import "../admin/tag_rules";
@import "../admin/terms_of_service_files";
@import "../admin/validation";
@import "../admin/variant_overrides";
@import "../admin/welcome";
@import "side_menu";
@import "tables";
@import "tag_rules";
@import "terms_of_service_files";
@import "validation";
@import "variant_overrides";
@import "welcome";
@import "shared/question-mark-icon";
@import "../admin/question-mark-tooltip";
@import "tom-select/src/scss/tom-select.default";
@import "~tom-select/src/scss/tom-select.default";
@import "components/tom_select"; // admin_v3
@import "modal_component/modal_component";
@import "vertical_ellipsis_menu_component/vertical_ellipsis_menu_component"; // admin_v3 and only V3
@import "tag_list_input_component/tag_list_input_component";
@import "admin/trix";
@import "trix";
@import "terms_of_service_banner"; // admin_v3

View File

@@ -1,110 +0,0 @@
#change_type {
section {
margin: 2em 0 0 0;
&,
& * {
color: $spree-blue;
}
}
.description {
background-color: $spree-light-blue;
margin-top: -2em;
padding: 4em 2em 2em 1em;
@media all and (max-width: 786px) {
margin-bottom: 2em;
}
}
.admin-cta {
border: 1px solid $spree-blue;
@include border-radius(3px);
text-align: center;
padding: 1em;
}
.error {
display: block;
color: #f57e80;
border: 1px solid #f57e80;
background-color: #fde6e7;
@include border-radius(3px);
margin-bottom: 1em;
padding: 0.5em;
}
a.selector {
position: relative;
border: 2px solid black;
text-align: center;
width: 100%;
cursor: pointer;
&,
& * {
color: white;
}
&:after,
&:before {
top: 100%;
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&:after {
border-color: rgba(136, 183, 213, 0);
border-top-color: $spree-blue;
border-width: 12px;
margin-left: -12px;
}
&:hover {
&:after {
border-top-color: $spree-green;
}
}
&:before {
border-color: rgba(84, 152, 218, 0);
border-top-color: black;
border-width: 15px;
margin-left: -15px;
}
.bottom {
background: repeating-linear-gradient(
60deg,
rgba(84, 152, 218, 0),
rgba(84, 152, 218, 0) 5px,
rgba(255, 255, 255, 0.25) 5px,
rgba(255, 255, 255, 0.25) 10px
);
margin-top: 1em;
margin-left: -15px;
margin-right: -15px;
padding: 5px;
text-transform: uppercase;
}
&.selected {
background-color: black;
&:after,
&:hover &:after {
border-top-color: black;
}
}
}
}

View File

@@ -8,7 +8,8 @@ table tbody tr {
}
&.action-remove td,
&.action-void td {
&.action-void td,
&.action-internal_void td {
text-decoration: line-through;
&.actions {

View File

@@ -2,14 +2,17 @@
text-align: center;
margin: 0 0 1em;
padding: 20px 0 28px 0;
background-color: $color-7;
display: flex;
justify-content: center;
gap: 8px;
button.page,
.pagelist {
display: flex;
gap: inherit;
list-style-type: none;
}
.page {
width: $btn-relaxed-height;
line-height: $btn-relaxed-height;
@@ -69,17 +72,6 @@
&:active {
border: none;
}
}
button {
margin: 0 0.35em;
background-color: $white;
color: $near-black;
box-shadow: $box-shadow;
&.active {
color: $white;
background-color: $red;
cursor: default;

View File

@@ -1,21 +0,0 @@
.dashboard_item.single-ent {
.header {
padding: 0.77778em 1.33333em 0.77778em 0.77778em;
height: auto !important;
}
.list {
.button.bottom {
width: 100%;
}
}
}
.button.big {
width: 100%;
font-size: 1rem;
@include border-radius(25px);
padding: 15px;
}

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