diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 37d5595022..7f9496dfe9 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,12 +2,11 @@ name: Tests
on:
push:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+ workflow_dispatch:
env:
+ CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
RAILS_ENV: test
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
@@ -16,7 +15,7 @@ env:
jobs:
rspec:
- runs-on: ubicloud-standard-2
+ runs-on: ubuntu-24.04
services:
postgres:
@@ -31,28 +30,22 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
-
redis:
image: redis:7.4.2
ports:
- 6389:6379
options: --entrypoint redis-server
-
strategy:
fail-fast: false
matrix:
ci_node_total: [2]
ci_node_index: [0, 1]
-
steps:
- uses: actions/checkout@v4
-
- name: Build setup
uses: ./.github/common/
-
- name: Setup test database
run: cd backend && bundle exec rails db:create db:schema:load
-
- name: Run tests
env:
RUBY_YJIT_ENABLE: 1
@@ -65,18 +58,14 @@ jobs:
playwright:
name: playwright
- runs-on: ubicloud-standard-4
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
-
- name: Build setup
uses: ./.github/common/
-
- run: pnpm puppeteer browsers install chrome
-
- run: node docker/createCertificate.js
-
- name: Cache Next build
id: next-cache
uses: actions/cache@v4
@@ -85,13 +74,10 @@ jobs:
key: next-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'backend/pnpm-lock.yaml', 'frontend/pnpm-lock.yaml') }}-${{ hashFiles('frontend/**/*.{ts,tsx}') }}
restore-keys: |
next-${{ runner.os }}-
-
- run: NODE_ENV=test pnpm run build-next --no-lint
shell: bash
-
- name: Install foreman
run: gem install foreman
-
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
@@ -100,20 +86,16 @@ jobs:
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'backend/pnpm-lock.yaml', 'frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-${{ runner.os }}-
-
- name: Install Playwright Browsers
run: pnpm playwright install --with-deps chromium
if: steps.playwright-cache.outputs.cache-hit != 'true'
-
- name: Run docker compose
run: docker compose -f docker/docker-compose-local-linux.yml up -d
-
- name: Run Playwright tests
run: pnpm playwright test
-
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
- retention-days: 7
+ retention-days: 7
\ No newline at end of file
diff --git a/backend/app/controllers/concerns/set_current.rb b/backend/app/controllers/concerns/set_current.rb
index 4a592ba64e..21fd0b398e 100644
--- a/backend/app/controllers/concerns/set_current.rb
+++ b/backend/app/controllers/concerns/set_current.rb
@@ -26,7 +26,7 @@ def set_current
if user && cookies["invitation_token"].present?
invite_link = CompanyInviteLink.find_by(token: cookies["invitation_token"])
invited_company = invite_link&.company
- user.update!(signup_invite_link: invite_link) if invite_link
+ AcceptCompanyInviteLink.new(user:, token: invite_link.token).perform if invite_link
cookies.delete("invitation_token")
end
diff --git a/backend/app/mailers/company_worker_mailer.rb b/backend/app/mailers/company_worker_mailer.rb
index 2d1cb607d6..ca18094f78 100644
--- a/backend/app/mailers/company_worker_mailer.rb
+++ b/backend/app/mailers/company_worker_mailer.rb
@@ -77,6 +77,17 @@ def payment_failed_reenter_bank_details(payment_id, amount, currency)
mail(to: user.email, reply_to: company.email, subject: "🔴 Payment failed: re-enter your bank details")
end
+ def payment_failed(payment_id, amount, currency)
+ @payment = Payment.find(payment_id)
+ @invoice = @payment.invoice
+ @currency = currency
+ @amount = amount
+ company = @invoice.company
+ user = @invoice.user
+
+ mail(to: user.email, reply_to: company.email, subject: "🔴 Payment failed: payment failure for invoice ##{@invoice.id}")
+ end
+
def equity_percent_selection(company_worker_id)
company_worker = CompanyWorker.find(company_worker_id)
@company = company_worker.company
diff --git a/backend/app/services/pay_investor_dividends.rb b/backend/app/services/pay_investor_dividends.rb
index 7db37dba20..a6383fa168 100644
--- a/backend/app/services/pay_investor_dividends.rb
+++ b/backend/app/services/pay_investor_dividends.rb
@@ -20,7 +20,6 @@ def process
user.tax_information_confirmed_at.nil? ||
user.bank_account_for_dividends.nil?
return unless user.has_verified_tax_id?
- raise "Feature unsupported for company #{company.id}" unless company.equity_enabled?
raise "Flexile balance insufficient to pay for dividends to investor #{company_investor.id}" unless Wise::AccountBalance.has_sufficient_flexile_balance?(net_amount_in_usd)
raise "Unknown country for user #{user.id}" if user.country_code.blank?
diff --git a/backend/app/services/pay_investor_equity_buybacks.rb b/backend/app/services/pay_investor_equity_buybacks.rb
index 80d6e98018..3954648b73 100644
--- a/backend/app/services/pay_investor_equity_buybacks.rb
+++ b/backend/app/services/pay_investor_equity_buybacks.rb
@@ -20,7 +20,6 @@ def process
user.tax_information_confirmed_at.nil?
return unless user.has_verified_tax_id?
- raise "Feature unsupported for company #{company.id}" unless company.equity_enabled?
raise "Flexile balance insufficient to pay for equity buybacks to investor #{company_investor.id}" unless Wise::AccountBalance.has_sufficient_flexile_balance?(net_amount_in_usd)
raise "Unknown country for user #{user.id}" if user.country_code.blank?
diff --git a/backend/app/services/pay_invoice.rb b/backend/app/services/pay_invoice.rb
index b320730465..ca56e4ee70 100644
--- a/backend/app/services/pay_invoice.rb
+++ b/backend/app/services/pay_invoice.rb
@@ -40,7 +40,6 @@ def process
account = payout_service.get_recipient_account(recipient_id: bank_account.recipient_id)
unless account["active"]
bank_account.mark_deleted!
- CompanyWorkerMailer.payment_failed_reenter_bank_details(payment.id, amount, target_currency).deliver_later
raise WiseError, "Bank account is no longer active for payment #{payment.id}"
end
quote = payout_service.create_quote(target_currency:, amount:, recipient_id: bank_account.recipient_id)
@@ -68,6 +67,16 @@ def process
rescue WiseError => e
payment.update!(status: Payment::FAILED)
invoice.update!(status: Invoice::FAILED)
+
+ target_currency = payment.wise_recipient&.currency || "USD"
+ amount = payment.cash_amount_in_usd
+
+ if e.message.include?("Bank account is no longer active")
+ CompanyWorkerMailer.payment_failed_reenter_bank_details(payment.id, amount, target_currency).deliver_later
+ else
+ CompanyWorkerMailer.payment_failed(payment.id, amount, target_currency).deliver_later
+ end
+
raise e
end
end
diff --git a/backend/app/sidekiq/wise_transfer_update_job.rb b/backend/app/sidekiq/wise_transfer_update_job.rb
index dad9774ad8..ea6e0091ee 100644
--- a/backend/app/sidekiq/wise_transfer_update_job.rb
+++ b/backend/app/sidekiq/wise_transfer_update_job.rb
@@ -36,6 +36,11 @@ def perform(params)
if payment.is_a?(Payment)
amount_cents = api_service.get_transfer(transfer_id:)["sourceValue"] * -100
payment.balance_transactions.create!(company: payment.company, amount_cents:, transaction_type: BalanceTransaction::PAYMENT_FAILED)
+
+ transfer_details = api_service.get_transfer(transfer_id:)
+ amount = transfer_details["targetValue"]
+ currency = transfer_details["targetCurrency"]
+ CompanyWorkerMailer.payment_failed(payment.id, amount, currency).deliver_later
end
end
invoice.update!(status: Invoice::FAILED)
diff --git a/backend/app/views/company_worker_mailer/payment_failed.html.erb b/backend/app/views/company_worker_mailer/payment_failed.html.erb
new file mode 100644
index 0000000000..020c346a09
--- /dev/null
+++ b/backend/app/views/company_worker_mailer/payment_failed.html.erb
@@ -0,0 +1,34 @@
+
+ There was an issue processing your payment of <%= Money.from_amount(@amount, @currency).format(with_currency: true, symbol: false, no_cents_if_whole: true) %>
+
+
+ Invoice ID
+
+ <%= @invoice.invoice_number %>
+
+ Invoice amount
+
+ <%= cents_format(@invoice.total_amount_in_usd_cents, with_currency: true, symbol: false) %>
+
+ Bank account
+
+ ****<%= @payment.recipient_last4 %>
+
+
+
+ Unfortunately, there was an issue processing your payment through Wise. This could be due to:
+
+
+ Incorrect bank account information
+ Bank transfer restrictions
+ Compliance or verification issues
+ Processing errors at the banking institution
+
+
+
+ Please log into your account to verify your bank details are correct and up-to-date. If your bank details appear correct, please contact support for assistance.
+
+
+
+ We'll attempt to process your payment again once the issue is resolved.
+
diff --git a/backend/app/views/user_mailer/otp_code.html.erb b/backend/app/views/user_mailer/otp_code.html.erb
index f369e9b77d..d85e3c332c 100644
--- a/backend/app/views/user_mailer/otp_code.html.erb
+++ b/backend/app/views/user_mailer/otp_code.html.erb
@@ -1,7 +1,3 @@
-Your verification code
-
-Your verification code is: <%= @otp_code %>
+Your verification code is <%= @otp_code %>
This code will expire in 10 minutes. If you didn't request this code, please ignore this email.
-
-If you're having trouble, contact us at <%= ApplicationMailer::SUPPORT_EMAIL %> .
diff --git a/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_creating_a_quote_fails_and_sends_a_notification.yml b/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_creating_a_quote_fails_and_sends_a_notification.yml
new file mode 100644
index 0000000000..9842406de2
--- /dev/null
+++ b/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_creating_a_quote_fails_and_sends_a_notification.yml
@@ -0,0 +1,278 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://api.sandbox.transferwise.tech/v1/accounts
+ body:
+ encoding: UTF-8
+ string: '{"currency":"GBP","type":"sort_code","details":{"legalType":"PRIVATE","email":"someone@somewhere.com","accountHolderName":"someone
+ somewhere","sortCode":231470,"accountNumber":28821822,"address":{"country":"GB","city":"London","firstLine":"112
+ 2nd street","postCode":"SW1P 3"}},"profile":""}'
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 08:07:31 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b547e13f6d9aa5-NAG
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Expires:
+ - '0'
+ Pragma:
+ - no-cache
+ X-Trace-Id:
+ - 1f99f28c972aa8929f31062564e00225
+ Route:
+ - v1_account_create
+ X-Envoy-Upstream-Service-Time:
+ - '245'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=ZJz0gN7A7ypbkxk048VSUDvyxgjl82gj6O30VZ2sniA-1754554051-1.0.1.1-7L9UsQrHABkAOWYUk5HSym7gNZlkwdOO6858paOK9yrFMwF_omtQGKbwIrg_V5m4ncMYC9MArRga2RuojVLNxFE4yQuUUBwnyGdmpgElKzY;
+ path=/; expires=Thu, 07-Aug-25 08:37:31 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"id":701432364,"business":null,"profile":,"accountHolderName":"someone
+ somewhere","currency":"GBP","country":"GB","type":"sort_code","details":{"address":{"country":"GB","countryCode":"GB","firstLine":"112
+ 2nd street","postCode":"SW1P 3","city":"London","state":null},"email":"someone@somewhere.com","legalType":"PRIVATE","accountHolderName":null,"accountNumber":"28821822","sortCode":"231470","abartn":null,"accountType":null,"bankgiroNumber":null,"ifscCode":null,"bsbCode":null,"institutionNumber":null,"transitNumber":null,"phoneNumber":null,"bankCode":null,"russiaRegion":null,"routingNumber":null,"branchCode":null,"cpf":null,"cardToken":null,"idType":null,"idNumber":null,"idCountryIso3":null,"idValidFrom":null,"idValidTo":null,"clabe":null,"swiftCode":null,"dateOfBirth":null,"clearingNumber":null,"bankName":"WISE
+ PAYMENTS LTD WISE PAYMENTS LIMITED","branchName":null,"businessNumber":null,"province":null,"city":null,"rut":null,"token":null,"cnpj":null,"payinReference":null,"pspReference":null,"orderId":null,"idDocumentType":null,"idDocumentNumber":null,"identificationNumber":null,"targetProfile":null,"targetUserId":null,"taxId":null,"job":null,"nationality":null,"interacAccount":null,"bban":null,"town":null,"postCode":null,"language":null,"billerCode":null,"customerReferenceNumber":null,"prefix":null,"relationship":null,"IBAN":null,"iban":null,"bic":null,"BIC":null},"user":13122309,"active":true,"ownedByCustomer":false,"confirmations":null}'
+ recorded_at: Thu, 07 Aug 2025 08:07:31 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v4/profiles//balances?types=STANDARD
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 08:07:31 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ X-Trace-Id:
+ - f32ac45f197201e8730ea983004c43ee
+ Route:
+ - v4_balance_get
+ X-Envoy-Upstream-Service-Time:
+ - '90'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=RVFdyCR8HTJUPQ7pl1aawsqNsWAxpbGCNBIvDMBBBVU-1754554051-1.0.1.1-7AirlgyFCPms_Bdzl0dl20mwWay69HF7zn8BN_xOiWmwBC.nYcuITPi.r6ipG5e_qtchJhCXaBfoPpAzPt99D4cGMINMGlo8sM7GVsAOK2g;
+ path=/; expires=Thu, 07-Aug-25 08:37:31 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Cf-Ray:
+ - 96b547e57e3f9a83-NAG
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '[{"id":285903,"currency":"GBP","amount":{"value":1000000.00,"currency":"GBP"},"reservedAmount":{"value":0.00,"currency":"GBP"},"cashAmount":{"value":1000000.00,"currency":"GBP"},"totalWorth":{"value":1000000.00,"currency":"GBP"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:38.170301Z","modificationTime":"2025-07-17T09:35:38.561910Z","visible":true,"primary":true,"groupId":null,"recipientId":701360838},{"id":285904,"currency":"EUR","amount":{"value":1000000.00,"currency":"EUR"},"reservedAmount":{"value":0.00,"currency":"EUR"},"cashAmount":{"value":1000000.00,"currency":"EUR"},"totalWorth":{"value":1000000.00,"currency":"EUR"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:38.926250Z","modificationTime":"2025-07-17T09:35:39.278111Z","visible":true,"primary":true,"groupId":null,"recipientId":701360839},{"id":285905,"currency":"USD","amount":{"value":1000000.00,"currency":"USD"},"reservedAmount":{"value":0.00,"currency":"USD"},"cashAmount":{"value":1000000.00,"currency":"USD"},"totalWorth":{"value":1000000.00,"currency":"USD"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:39.696822Z","modificationTime":"2025-07-17T09:35:40.030487Z","visible":true,"primary":true,"groupId":null,"recipientId":701360840},{"id":285855,"currency":"AUD","amount":{"value":1000000.00,"currency":"AUD"},"reservedAmount":{"value":0.00,"currency":"AUD"},"cashAmount":{"value":1000000.00,"currency":"AUD"},"totalWorth":{"value":1000000.00,"currency":"AUD"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:40.382713Z","modificationTime":"2025-07-17T09:35:40.701227Z","visible":true,"primary":true,"groupId":null,"recipientId":701360841}]'
+ recorded_at: Thu, 07 Aug 2025 08:07:31 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v1/rates?source=USD&target=GBP
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 08:07:31 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b547e7bcf49aa7-NAG
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ X-Trace-Id:
+ - 1d15a6dc955f1295de61330bb2aa53ff
+ Access-Control-Allow-Origin:
+ - "*"
+ Route:
+ - rates
+ X-Envoy-Upstream-Service-Time:
+ - '18'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=BZTnYebp58P655Tx8SE_rJQl0pKbsMWJnnsa_.3wTV8-1754554051-1.0.1.1-mlp0rCI5BmhmufGTyLI7vkdLHgBpSLkXJZ3gXpI_iISjL1ZQPUsD3OFkVylDuAmWMv_AaG4.a8azUXvWYBStgusIpEp.KMviDnwIWDt0x24;
+ path=/; expires=Thu, 07-Aug-25 08:37:31 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '[{"rate":0.747584,"source":"USD","target":"GBP","time":"2025-08-07T08:04:38+0000"}]'
+ recorded_at: Thu, 07 Aug 2025 08:07:31 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v1/accounts/701432364
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 08:07:32 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b547e94c499a75-NAG
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Trace-Id:
+ - fe0d40e95aefcb71cac5d87104254752
+ Route:
+ - account_details_v1_accounts_get
+ X-Envoy-Upstream-Service-Time:
+ - '50'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=1sSG4jSNAkDkN.Eea75uZF6os.pMZStx8hmkgBHpjCA-1754554052-1.0.1.1-KhVUUfKKMnCQ10SHI5b6QinG1Qkuga_ckt4yzSdwUkwzlPp3VSpSUmODpczpBUc4QFljlrbSLxc_OvCG8FQwDo0NmCpTjVt7FvRoqGZY6AM;
+ path=/; expires=Thu, 07-Aug-25 08:37:32 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"id":701432364,"business":null,"profile":,"accountHolderName":"someone
+ somewhere","currency":"GBP","country":"GB","type":"sort_code","details":{"address":{"country":"GB","countryCode":"GB","firstLine":"112
+ 2nd street","postCode":"SW1P 3","city":"London","state":null},"email":"someone@somewhere.com","legalType":"PRIVATE","accountHolderName":null,"accountNumber":"28821822","sortCode":"231470","abartn":null,"accountType":null,"bankgiroNumber":null,"ifscCode":null,"bsbCode":null,"institutionNumber":null,"transitNumber":null,"phoneNumber":null,"bankCode":null,"russiaRegion":null,"routingNumber":null,"branchCode":null,"cpf":null,"cardToken":null,"idType":null,"idNumber":null,"idCountryIso3":null,"idValidFrom":null,"idValidTo":null,"clabe":null,"swiftCode":null,"dateOfBirth":null,"clearingNumber":null,"bankName":null,"branchName":null,"businessNumber":null,"province":null,"city":null,"rut":null,"token":null,"cnpj":null,"payinReference":null,"pspReference":null,"orderId":null,"idDocumentType":null,"idDocumentNumber":null,"identificationNumber":null,"targetProfile":null,"targetUserId":null,"taxId":null,"job":null,"nationality":null,"interacAccount":null,"bban":null,"town":null,"postCode":null,"language":null,"billerCode":null,"customerReferenceNumber":null,"prefix":null,"relationship":null,"IBAN":null,"iban":null,"bic":null,"BIC":null},"user":13122309,"active":true,"ownedByCustomer":false,"confirmations":null}'
+ recorded_at: Thu, 07 Aug 2025 08:07:32 GMT
+recorded_with: VCR 6.3.1
diff --git a/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_creating_a_transfer_fails_and_sends_a_notification.yml b/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_creating_a_transfer_fails_and_sends_a_notification.yml
new file mode 100644
index 0000000000..f7219ca324
--- /dev/null
+++ b/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_creating_a_transfer_fails_and_sends_a_notification.yml
@@ -0,0 +1,433 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://api.sandbox.transferwise.tech/v1/accounts
+ body:
+ encoding: UTF-8
+ string: '{"currency":"GBP","type":"sort_code","details":{"legalType":"PRIVATE","email":"someone@somewhere.com","accountHolderName":"someone
+ somewhere","sortCode":231470,"accountNumber":28821822,"address":{"country":"GB","city":"London","firstLine":"112
+ 2nd street","postCode":"SW1P 3"}},"profile":""}'
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:35 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Trace-Id:
+ - c276b905517818251e388058097808ba
+ Route:
+ - v1_account_create
+ X-Envoy-Upstream-Service-Time:
+ - '131'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=4AYbPojOK6cB_uFcfQCJc7OKQK2jt9ox5uXFWE6dhAo-1754559035-1.0.1.1-6gCTFi14BtQPD_ymkwIQtgJfqkCSkgsb26lal_4DdNUxHMegy0ae23wyJdl.O30cNEQwr2XL.ZQ0r5yI6KygnNPy292LeasrVjIUsY5Obhg;
+ path=/; expires=Thu, 07-Aug-25 10:00:35 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Cf-Ray:
+ - 96b5c191d9c19a72-NAG
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"id":701432364,"business":null,"profile":,"accountHolderName":"someone
+ somewhere","currency":"GBP","country":"GB","type":"sort_code","details":{"address":{"country":"GB","countryCode":"GB","firstLine":"112
+ 2nd street","postCode":"SW1P 3","city":"London","state":null},"email":"someone@somewhere.com","legalType":"PRIVATE","accountHolderName":null,"accountNumber":"28821822","sortCode":"231470","abartn":null,"accountType":null,"bankgiroNumber":null,"ifscCode":null,"bsbCode":null,"institutionNumber":null,"transitNumber":null,"phoneNumber":null,"bankCode":null,"russiaRegion":null,"routingNumber":null,"branchCode":null,"cpf":null,"cardToken":null,"idType":null,"idNumber":null,"idCountryIso3":null,"idValidFrom":null,"idValidTo":null,"clabe":null,"swiftCode":null,"dateOfBirth":null,"clearingNumber":null,"bankName":null,"branchName":null,"businessNumber":null,"province":null,"city":null,"rut":null,"token":null,"cnpj":null,"payinReference":null,"pspReference":null,"orderId":null,"idDocumentType":null,"idDocumentNumber":null,"identificationNumber":null,"targetProfile":null,"targetUserId":null,"taxId":null,"job":null,"nationality":null,"interacAccount":null,"bban":null,"town":null,"postCode":null,"language":null,"billerCode":null,"customerReferenceNumber":null,"prefix":null,"relationship":null,"IBAN":null,"iban":null,"bic":null,"BIC":null},"user":13122309,"active":true,"ownedByCustomer":false,"confirmations":null}'
+ recorded_at: Thu, 07 Aug 2025 09:30:35 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v4/profiles//balances?types=STANDARD
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:36 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b5c1957f6a9a8d-NAG
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Expires:
+ - '0'
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Pragma:
+ - no-cache
+ X-Trace-Id:
+ - 4213c2b3dcb81d4a36400e2aca2e82e7
+ Route:
+ - v4_balance_get
+ X-Envoy-Upstream-Service-Time:
+ - '97'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=HPJj05cCgEWzIDAzZjbKWYSbALaLYzLgs7TagIaL6ZY-1754559036-1.0.1.1-lLCFahYJgZJ2wWpWGHCuo45BH9w9sEjYB5wwDU.mD0iN0lccO1C4Lt6ZOo0LJppRk196BAgMIPwQLED8sw9floOgSO9lPWDmh9Q7_yoxpr8;
+ path=/; expires=Thu, 07-Aug-25 10:00:36 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '[{"id":285903,"currency":"GBP","amount":{"value":1000000.00,"currency":"GBP"},"reservedAmount":{"value":0.00,"currency":"GBP"},"cashAmount":{"value":1000000.00,"currency":"GBP"},"totalWorth":{"value":1000000.00,"currency":"GBP"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:38.170301Z","modificationTime":"2025-07-17T09:35:38.561910Z","visible":true,"primary":true,"groupId":null,"recipientId":701360838},{"id":285904,"currency":"EUR","amount":{"value":1000000.00,"currency":"EUR"},"reservedAmount":{"value":0.00,"currency":"EUR"},"cashAmount":{"value":1000000.00,"currency":"EUR"},"totalWorth":{"value":1000000.00,"currency":"EUR"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:38.926250Z","modificationTime":"2025-07-17T09:35:39.278111Z","visible":true,"primary":true,"groupId":null,"recipientId":701360839},{"id":285905,"currency":"USD","amount":{"value":1000000.00,"currency":"USD"},"reservedAmount":{"value":0.00,"currency":"USD"},"cashAmount":{"value":1000000.00,"currency":"USD"},"totalWorth":{"value":1000000.00,"currency":"USD"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:39.696822Z","modificationTime":"2025-07-17T09:35:40.030487Z","visible":true,"primary":true,"groupId":null,"recipientId":701360840},{"id":285855,"currency":"AUD","amount":{"value":1000000.00,"currency":"AUD"},"reservedAmount":{"value":0.00,"currency":"AUD"},"cashAmount":{"value":1000000.00,"currency":"AUD"},"totalWorth":{"value":1000000.00,"currency":"AUD"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:40.382713Z","modificationTime":"2025-07-17T09:35:40.701227Z","visible":true,"primary":true,"groupId":null,"recipientId":701360841}]'
+ recorded_at: Thu, 07 Aug 2025 09:30:36 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v1/rates?source=USD&target=GBP
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:36 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b5c197c8fb9aa5-NAG
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Expires:
+ - '0'
+ Pragma:
+ - no-cache
+ X-Trace-Id:
+ - 77f9589cca66fcda2108b8c6499db62a
+ Access-Control-Allow-Origin:
+ - "*"
+ Route:
+ - rates
+ X-Envoy-Upstream-Service-Time:
+ - '20'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=wqq3aJw0AH2fy3PpKRWPBE10i4pzAeuXMgVYvpqmMWs-1754559036-1.0.1.1-HoK4FmYhW0s4WC4ikuA2hsdZ7KmtQ2alfOyFbc3TP4PcHe1qE9ZCo.yUTbmVW.9iWH0YFdcYQ9qNQ75YqWxM3BPOc9N2j63xKIWXxJtfAig;
+ path=/; expires=Thu, 07-Aug-25 10:00:36 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '[{"rate":0.747885,"source":"USD","target":"GBP","time":"2025-08-07T09:29:42+0000"}]'
+ recorded_at: Thu, 07 Aug 2025 09:30:36 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v1/accounts/701432364
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:36 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b5c19bc9cf9a6f-NAG
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Trace-Id:
+ - 20c0b12c4970d1108fce043bc419db4e
+ Route:
+ - account_details_v1_accounts_get
+ X-Envoy-Upstream-Service-Time:
+ - '41'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=l7dUZdS.7WmpIgzQ4c6NgjS77emQhzAGABlrSftZLS0-1754559036-1.0.1.1-MDspZG1kj2Xu4Cxv32bXV_r.Bn1rK9itQaUzLq2wvgPC8WYkBk9C.xD9QQa0BM2dBI4yrk3V1Tud3zeERaDfO4MbkPx5peYQ69Y8QiVKFVI;
+ path=/; expires=Thu, 07-Aug-25 10:00:36 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"id":701432364,"business":null,"profile":,"accountHolderName":"someone
+ somewhere","currency":"GBP","country":"GB","type":"sort_code","details":{"address":{"country":"GB","countryCode":"GB","firstLine":"112
+ 2nd street","postCode":"SW1P 3","city":"London","state":null},"email":"someone@somewhere.com","legalType":"PRIVATE","accountHolderName":null,"accountNumber":"28821822","sortCode":"231470","abartn":null,"accountType":null,"bankgiroNumber":null,"ifscCode":null,"bsbCode":null,"institutionNumber":null,"transitNumber":null,"phoneNumber":null,"bankCode":null,"russiaRegion":null,"routingNumber":null,"branchCode":null,"cpf":null,"cardToken":null,"idType":null,"idNumber":null,"idCountryIso3":null,"idValidFrom":null,"idValidTo":null,"clabe":null,"swiftCode":null,"dateOfBirth":null,"clearingNumber":null,"bankName":null,"branchName":null,"businessNumber":null,"province":null,"city":null,"rut":null,"token":null,"cnpj":null,"payinReference":null,"pspReference":null,"orderId":null,"idDocumentType":null,"idDocumentNumber":null,"identificationNumber":null,"targetProfile":null,"targetUserId":null,"taxId":null,"job":null,"nationality":null,"interacAccount":null,"bban":null,"town":null,"postCode":null,"language":null,"billerCode":null,"customerReferenceNumber":null,"prefix":null,"relationship":null,"IBAN":null,"iban":null,"bic":null,"BIC":null},"user":13122309,"active":true,"ownedByCustomer":false,"confirmations":null}'
+ recorded_at: Thu, 07 Aug 2025 09:30:36 GMT
+- request:
+ method: post
+ uri: https://api.sandbox.transferwise.tech/v3/profiles//quotes
+ body:
+ encoding: UTF-8
+ string: '{"sourceAmount":null,"targetAmount":44.8731,"sourceCurrency":"USD","targetAccount":"701432364","targetCurrency":"GBP","profileId":"","preferredPayIn":"BALANCE"}'
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:37 GMT
+ Content-Type:
+ - application/json
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Trace-Id:
+ - 4e53f2b00e5523470c7bd4657e493d56
+ Route:
+ - v3_quotes_quote_create
+ X-Envoy-Upstream-Service-Time:
+ - '630'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=Rqd9X8kib__65oo2fn6nd8yGOO5UTcVZXiDmgSBXTe0-1754559037-1.0.1.1-.8fPnYHZ6DhvXRM8HhR8FnIgSVSPZtNN13gSwwh_5xAbkxC.4h3AqWIOXwcM0RD4FOkl2eDz3eZGtalrffz7BG.2eGvs1x2bGcLPALDj42w;
+ path=/; expires=Thu, 07-Aug-25 10:00:37 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Cf-Ray:
+ - 96b5c19dbc569a9f-NAG
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"targetAmount":44.87,"guaranteedTargetAmountAllowed":false,"targetAmountAllowed":true,"preferredPayIn":"BALANCE","paymentOptions":[{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0130,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"BALANCE","price":{"priceSetId":558,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":0.79,"currency":"USD","label":"0.79 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.0,"currency":"USD","label":"0
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":0.79,"currency":"USD","label":"0.79
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":0.79,"payIn":0.0,"discount":0,"total":0.79,"priceSetId":558,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":60.79,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0789,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"GOOGLE_PAY","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.14,"currency":"USD","label":"5.14 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.02,"currency":"USD","label":"4.02
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":4.02,"discount":0,"total":5.14,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":65.14,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0789,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"APPLE_PAY","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.14,"currency":"USD","label":"5.14 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.02,"currency":"USD","label":"4.02
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":4.02,"discount":0,"total":5.14,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":65.14,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0789,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.14,"currency":"USD","label":"5.14 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.02,"currency":"USD","label":"4.02
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":4.02,"discount":0,"total":5.14,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":65.14,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0207,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"DIRECT_DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.27,"currency":"USD","label":"1.27 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.15,"currency":"USD","label":"0.15
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":0.15,"discount":0,"total":1.27,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":61.27,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ 5 hours","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.1075,"estimatedDelivery":"2025-08-07T14:20:00Z","payIn":"BANK_TRANSFER","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":7.23,"currency":"USD","label":"7.23 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":6.11,"currency":"USD","label":"6.11
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":6.11,"discount":0,"total":7.23,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":67.23,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"by
+ Thursday, August 14","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.1075,"estimatedDelivery":"2025-08-14T18:00:00Z","payIn":"SWIFT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":7.23,"currency":"USD","label":"7.23 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":6.11,"currency":"USD","label":"6.11
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":6.11,"discount":0,"total":7.23,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":67.23,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0663,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"INTERNATIONAL_DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":4.26,"currency":"USD","label":"4.26 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":3.14,"currency":"USD","label":"3.14
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":3.14,"discount":0,"total":4.26,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":64.26,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0792,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"MC_BUSINESS_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.16,"currency":"USD","label":"5.16 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.04,"currency":"USD","label":"4.04
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":4.04,"discount":0,"total":5.16,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":65.16,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"MC_DEBIT_OR_PREPAID","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"CARD","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"MAESTRO","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0475,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"INT_DEBIT_WITH_EUROPEAN_CARD","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":2.99,"currency":"USD","label":"2.99 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":1.87,"currency":"USD","label":"1.87
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":1.87,"discount":0,"total":2.99,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":62.99,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0475,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"INT_CREDIT_WITH_EUROPEAN_CARD","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":2.99,"currency":"USD","label":"2.99 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":1.87,"currency":"USD","label":"1.87
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":1.87,"discount":0,"total":2.99,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":62.99,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0335,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"VISA_BUSINESS_DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":2.08,"currency":"USD","label":"2.08 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.96,"currency":"USD","label":"0.96
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":0.96,"discount":0,"total":2.08,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":62.08,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"VISA_DEBIT_OR_PREPAID","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0789,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"MC_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.14,"currency":"USD","label":"5.14 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.02,"currency":"USD","label":"4.02
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":4.02,"discount":0,"total":5.14,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":65.14,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0532,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"MC_BUSINESS_DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":3.37,"currency":"USD","label":"3.37 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":2.25,"currency":"USD","label":"2.25
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":2.25,"discount":0,"total":3.37,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":63.37,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0820,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"VISA_BUSINESS_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.36,"currency":"USD","label":"5.36 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.24,"currency":"USD","label":"4.24
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":4.24,"discount":0,"total":5.36,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":65.36,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0663,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"INTERNATIONAL_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":4.26,"currency":"USD","label":"4.26 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":3.14,"currency":"USD","label":"3.14
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":3.14,"discount":0,"total":4.26,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":64.26,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0699,"estimatedDelivery":"2025-08-07T09:30:37Z","payIn":"VISA_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":4.51,"currency":"USD","label":"4.51 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":3.39,"currency":"USD","label":"3.39
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f78fe-650c-4239-0a31-a929d1bb37f5"},"fee":{"transferwise":1.12,"payIn":3.39,"discount":0,"total":4.51,"priceSetId":556,"partner":0.0},"payOut":"BANK_TRANSFER","sourceAmount":64.51,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","disabled":false}],"notices":[],"transferFlowConfig":{"highAmount":{"showFeePercentage":false,"trackAsHighAmountSender":false,"showEducationStep":false,"offerPrefundingOption":false,"overLimitThroughCs":false,"overLimitThroughWiseAccount":false}},"rateTimestamp":"2025-08-07T09:29:42Z","targetAccountDetailsOwnedByProfile":false,"clientId":"transferwise-personal-tokens","guaranteedTargetAmount":false,"rateExpirationTime":"2025-08-07T21:59:59Z","providedAmountType":"TARGET","targetAccount":701432364,"payInCountry":"DE","payOutCountry":"GB","createdTime":"2025-08-07T09:30:37Z","rateType":"FIXED","payOut":"BANK_TRANSFER","funding":"POST","user":13122309,"profile":,"sourceCurrency":"USD","targetCurrency":"GBP","rate":0.747885,"status":"PENDING","expirationTime":"2025-08-07T10:00:37Z","id":"ce5475b8-5809-4ec9-9039-25d6ae9b74e5","type":"REGULAR"}'
+ recorded_at: Thu, 07 Aug 2025 09:30:37 GMT
+recorded_with: VCR 6.3.1
diff --git a/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_funding_fails_and_sends_a_notification.yml b/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_funding_fails_and_sends_a_notification.yml
new file mode 100644
index 0000000000..f56e860a8f
--- /dev/null
+++ b/backend/spec/fixtures/vcr_cassettes/PayInvoice/when_payment_method_is_setup/errors/marks_the_invoice_and_payment_as_failed_if_funding_fails_and_sends_a_notification.yml
@@ -0,0 +1,502 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://api.sandbox.transferwise.tech/v1/accounts
+ body:
+ encoding: UTF-8
+ string: '{"currency":"GBP","type":"sort_code","details":{"legalType":"PRIVATE","email":"someone@somewhere.com","accountHolderName":"someone
+ somewhere","sortCode":231470,"accountNumber":28821822,"address":{"country":"GB","city":"London","firstLine":"112
+ 2nd street","postCode":"SW1P 3"}},"profile":""}'
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:38 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Trace-Id:
+ - 9ef19de0318f7d75b9b50b997015ee46
+ Route:
+ - v1_account_create
+ X-Envoy-Upstream-Service-Time:
+ - '120'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=8WDI9LrI4RHSOEv4u.EwOAp1xtawT8xNFJXxOviieNc-1754559038-1.0.1.1-TI5EmpkmTif_SZ.JZrlFvjp938yHz893M7NjnBdMjLTM1rhcpJWWHYzzWdBLNWtwjm.o.AEww63.UX69GuSU2SP9or2lRrmd9eW6tPsFVfQ;
+ path=/; expires=Thu, 07-Aug-25 10:00:38 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Cf-Ray:
+ - 96b5c1a51c659a8a-NAG
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"id":701432364,"business":null,"profile":,"accountHolderName":"someone
+ somewhere","currency":"GBP","country":"GB","type":"sort_code","details":{"address":{"country":"GB","countryCode":"GB","firstLine":"112
+ 2nd street","postCode":"SW1P 3","city":"London","state":null},"email":"someone@somewhere.com","legalType":"PRIVATE","accountHolderName":null,"accountNumber":"28821822","sortCode":"231470","abartn":null,"accountType":null,"bankgiroNumber":null,"ifscCode":null,"bsbCode":null,"institutionNumber":null,"transitNumber":null,"phoneNumber":null,"bankCode":null,"russiaRegion":null,"routingNumber":null,"branchCode":null,"cpf":null,"cardToken":null,"idType":null,"idNumber":null,"idCountryIso3":null,"idValidFrom":null,"idValidTo":null,"clabe":null,"swiftCode":null,"dateOfBirth":null,"clearingNumber":null,"bankName":null,"branchName":null,"businessNumber":null,"province":null,"city":null,"rut":null,"token":null,"cnpj":null,"payinReference":null,"pspReference":null,"orderId":null,"idDocumentType":null,"idDocumentNumber":null,"identificationNumber":null,"targetProfile":null,"targetUserId":null,"taxId":null,"job":null,"nationality":null,"interacAccount":null,"bban":null,"town":null,"postCode":null,"language":null,"billerCode":null,"customerReferenceNumber":null,"prefix":null,"relationship":null,"IBAN":null,"iban":null,"bic":null,"BIC":null},"user":13122309,"active":true,"ownedByCustomer":false,"confirmations":null}'
+ recorded_at: Thu, 07 Aug 2025 09:30:38 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v4/profiles//balances?types=STANDARD
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:38 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b5c1a7ac039a99-NAG
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Trace-Id:
+ - 7da7b06cb43821e854249b9e0e6f3a08
+ Route:
+ - v4_balance_get
+ X-Envoy-Upstream-Service-Time:
+ - '91'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=Q7nl.415f9Ee0Gj8jEif3j0B__nuqoltqEAgY24JEIY-1754559038-1.0.1.1-w89pck5cgUwPmae4_wo1Bgkmp4vKF5h8rEA3rlh30KAk4ELN7YlglmJHkp8n67ZBasb8hJWapNC0U_zutAbwBrp0xCmxZ_D57tNosS1H.2M;
+ path=/; expires=Thu, 07-Aug-25 10:00:38 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '[{"id":285903,"currency":"GBP","amount":{"value":1000000.00,"currency":"GBP"},"reservedAmount":{"value":0.00,"currency":"GBP"},"cashAmount":{"value":1000000.00,"currency":"GBP"},"totalWorth":{"value":1000000.00,"currency":"GBP"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:38.170301Z","modificationTime":"2025-07-17T09:35:38.561910Z","visible":true,"primary":true,"groupId":null,"recipientId":701360838},{"id":285904,"currency":"EUR","amount":{"value":1000000.00,"currency":"EUR"},"reservedAmount":{"value":0.00,"currency":"EUR"},"cashAmount":{"value":1000000.00,"currency":"EUR"},"totalWorth":{"value":1000000.00,"currency":"EUR"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:38.926250Z","modificationTime":"2025-07-17T09:35:39.278111Z","visible":true,"primary":true,"groupId":null,"recipientId":701360839},{"id":285905,"currency":"USD","amount":{"value":1000000.00,"currency":"USD"},"reservedAmount":{"value":0.00,"currency":"USD"},"cashAmount":{"value":1000000.00,"currency":"USD"},"totalWorth":{"value":1000000.00,"currency":"USD"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:39.696822Z","modificationTime":"2025-07-17T09:35:40.030487Z","visible":true,"primary":true,"groupId":null,"recipientId":701360840},{"id":285855,"currency":"AUD","amount":{"value":1000000.00,"currency":"AUD"},"reservedAmount":{"value":0.00,"currency":"AUD"},"cashAmount":{"value":1000000.00,"currency":"AUD"},"totalWorth":{"value":1000000.00,"currency":"AUD"},"type":"STANDARD","name":null,"icon":null,"investmentState":"NOT_INVESTED","creationTime":"2025-07-17T09:35:40.382713Z","modificationTime":"2025-07-17T09:35:40.701227Z","visible":true,"primary":true,"groupId":null,"recipientId":701360841}]'
+ recorded_at: Thu, 07 Aug 2025 09:30:38 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v1/rates?source=USD&target=GBP
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:39 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b5c1a9da409a8a-NAG
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Expires:
+ - '0'
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Pragma:
+ - no-cache
+ X-Trace-Id:
+ - 2e08ff05c8747ac2d571043714a820d1
+ Access-Control-Allow-Origin:
+ - "*"
+ Route:
+ - rates
+ X-Envoy-Upstream-Service-Time:
+ - '17'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=Cq5VXSjJLICnBJV6vZ.dbymJFxSjrdeS5d13HkO08S8-1754559039-1.0.1.1-7ydAPEwPHpOi6.6msJeTo.2KvYNsQWH9DYccLRqZy4zJQhpGHLt1SuFqrb9uI5G9YwPqVH02XUw.0VLDckPnakM1ZbK2vuNgZqg9Jht8XH8;
+ path=/; expires=Thu, 07-Aug-25 10:00:39 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '[{"rate":0.747885,"source":"USD","target":"GBP","time":"2025-08-07T09:29:42+0000"}]'
+ recorded_at: Thu, 07 Aug 2025 09:30:39 GMT
+- request:
+ method: get
+ uri: https://api.sandbox.transferwise.tech/v1/accounts/701432364
+ body:
+ encoding: US-ASCII
+ string: ''
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:39 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b5c1ab9c5f9a8a-NAG
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Pragma:
+ - no-cache
+ Expires:
+ - '0'
+ X-Trace-Id:
+ - 3b5e8a13a7fc51733d2c77d454ebd591
+ Route:
+ - account_details_v1_accounts_get
+ X-Envoy-Upstream-Service-Time:
+ - '42'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=2G9XfbUsvEnRGAhH.kVjD_ADwvuEpiFpVGKVoi7sZsk-1754559039-1.0.1.1-To2cNlpvFNeNZhH9E2z4qjEMb1T3l3dRXSZMhnRC4GJJBconkNv8ArK4LG7jAUvqMenFiBsbsEoHxgQIZQNRNZfsU9C9NCTyBX19VS2JSO8;
+ path=/; expires=Thu, 07-Aug-25 10:00:39 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"id":701432364,"business":null,"profile":,"accountHolderName":"someone
+ somewhere","currency":"GBP","country":"GB","type":"sort_code","details":{"address":{"country":"GB","countryCode":"GB","firstLine":"112
+ 2nd street","postCode":"SW1P 3","city":"London","state":null},"email":"someone@somewhere.com","legalType":"PRIVATE","accountHolderName":null,"accountNumber":"28821822","sortCode":"231470","abartn":null,"accountType":null,"bankgiroNumber":null,"ifscCode":null,"bsbCode":null,"institutionNumber":null,"transitNumber":null,"phoneNumber":null,"bankCode":null,"russiaRegion":null,"routingNumber":null,"branchCode":null,"cpf":null,"cardToken":null,"idType":null,"idNumber":null,"idCountryIso3":null,"idValidFrom":null,"idValidTo":null,"clabe":null,"swiftCode":null,"dateOfBirth":null,"clearingNumber":null,"bankName":null,"branchName":null,"businessNumber":null,"province":null,"city":null,"rut":null,"token":null,"cnpj":null,"payinReference":null,"pspReference":null,"orderId":null,"idDocumentType":null,"idDocumentNumber":null,"identificationNumber":null,"targetProfile":null,"targetUserId":null,"taxId":null,"job":null,"nationality":null,"interacAccount":null,"bban":null,"town":null,"postCode":null,"language":null,"billerCode":null,"customerReferenceNumber":null,"prefix":null,"relationship":null,"IBAN":null,"iban":null,"bic":null,"BIC":null},"user":13122309,"active":true,"ownedByCustomer":false,"confirmations":null}'
+ recorded_at: Thu, 07 Aug 2025 09:30:39 GMT
+- request:
+ method: post
+ uri: https://api.sandbox.transferwise.tech/v3/profiles//quotes
+ body:
+ encoding: UTF-8
+ string: '{"sourceAmount":null,"targetAmount":44.8731,"sourceCurrency":"USD","targetAccount":"701432364","targetCurrency":"GBP","profileId":"","preferredPayIn":"BALANCE"}'
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:40 GMT
+ Content-Type:
+ - application/json
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Expires:
+ - '0'
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Pragma:
+ - no-cache
+ X-Trace-Id:
+ - d9cf8ddd4d7c85a5feae1bf12d6013c6
+ Route:
+ - v3_quotes_quote_create
+ X-Envoy-Upstream-Service-Time:
+ - '245'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=O6KD4wZq3ma65TBUqhLol2WUZmoJ8t3qIV7uqxYmejw-1754559040-1.0.1.1-vf7COvw7z1YYr6wOIN8L3tVv4LJiHfzae0sjwWeFoT.xrdVK2RTsrttjbbmIrysQRAutlQNHQ7ul6AlhHRz_et6NZ.ceKedn0ia9tvLZ_i8;
+ path=/; expires=Thu, 07-Aug-25 10:00:40 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Cf-Ray:
+ - 96b5c1ae3c009a99-NAG
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"targetAmount":44.87,"guaranteedTargetAmountAllowed":false,"targetAmountAllowed":true,"preferredPayIn":"BALANCE","paymentOptions":[{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0130,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":60.79,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"BALANCE","price":{"priceSetId":558,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":0.79,"currency":"USD","label":"0.79 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.0,"currency":"USD","label":"0
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":0.79,"currency":"USD","label":"0.79
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":0.79,"payIn":0.0,"discount":0,"total":0.79,"priceSetId":558,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0789,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":65.14,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"GOOGLE_PAY","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.14,"currency":"USD","label":"5.14 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.02,"currency":"USD","label":"4.02
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":4.02,"discount":0,"total":5.14,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0789,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":65.14,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"APPLE_PAY","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.14,"currency":"USD","label":"5.14 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.02,"currency":"USD","label":"4.02
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":4.02,"discount":0,"total":5.14,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0789,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":65.14,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.14,"currency":"USD","label":"5.14 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.02,"currency":"USD","label":"4.02
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":4.02,"discount":0,"total":5.14,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0207,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":61.27,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"DIRECT_DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.27,"currency":"USD","label":"1.27 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.15,"currency":"USD","label":"0.15
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":0.15,"discount":0,"total":1.27,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ 5 hours","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.1075,"estimatedDelivery":"2025-08-07T14:20:00Z","sourceAmount":67.23,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"BANK_TRANSFER","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":7.23,"currency":"USD","label":"7.23 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":6.11,"currency":"USD","label":"6.11
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":6.11,"discount":0,"total":7.23,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"by
+ Thursday, August 14","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.1075,"estimatedDelivery":"2025-08-14T18:00:00Z","sourceAmount":67.23,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"SWIFT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":7.23,"currency":"USD","label":"7.23 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":6.11,"currency":"USD","label":"6.11
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":6.11,"discount":0,"total":7.23,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0663,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":64.26,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"INTERNATIONAL_DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":4.26,"currency":"USD","label":"4.26 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":3.14,"currency":"USD","label":"3.14
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":3.14,"discount":0,"total":4.26,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0792,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":65.16,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"MC_BUSINESS_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.16,"currency":"USD","label":"5.16 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.04,"currency":"USD","label":"4.04
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":4.04,"discount":0,"total":5.16,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"MC_DEBIT_OR_PREPAID","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"CARD","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"MAESTRO","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0475,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":62.99,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"INT_DEBIT_WITH_EUROPEAN_CARD","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":2.99,"currency":"USD","label":"2.99 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":1.87,"currency":"USD","label":"1.87
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":1.87,"discount":0,"total":2.99,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0475,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":62.99,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"INT_CREDIT_WITH_EUROPEAN_CARD","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":2.99,"currency":"USD","label":"2.99 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":1.87,"currency":"USD","label":"1.87
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":1.87,"discount":0,"total":2.99,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0335,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":62.08,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"VISA_BUSINESS_DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":2.08,"currency":"USD","label":"2.08 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.96,"currency":"USD","label":"0.96
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":0.96,"discount":0,"total":2.08,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0310,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":61.92,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"VISA_DEBIT_OR_PREPAID","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":1.92,"currency":"USD","label":"1.92 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":0.8,"currency":"USD","label":"0.80
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":0.8,"discount":0,"total":1.92,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0789,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":65.14,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"MC_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.14,"currency":"USD","label":"5.14 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.02,"currency":"USD","label":"4.02
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":4.02,"discount":0,"total":5.14,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0532,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":63.37,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"MC_BUSINESS_DEBIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":3.37,"currency":"USD","label":"3.37 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":2.25,"currency":"USD","label":"2.25
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":2.25,"discount":0,"total":3.37,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0820,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":65.36,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"VISA_BUSINESS_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":5.36,"currency":"USD","label":"5.36 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":4.24,"currency":"USD","label":"4.24
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":4.24,"discount":0,"total":5.36,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0663,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":64.26,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"INTERNATIONAL_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":4.26,"currency":"USD","label":"4.26 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":3.14,"currency":"USD","label":"3.14
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":3.14,"discount":0,"total":4.26,"priceSetId":556,"partner":0.0},"disabled":false},{"formattedEstimatedDelivery":"in
+ seconds","estimatedDeliveryDelays":[],"allowedProfileTypes":["PERSONAL","BUSINESS"],"feePercentage":0.0699,"estimatedDelivery":"2025-08-07T09:30:39Z","sourceAmount":64.51,"targetAmount":44.87,"sourceCurrency":"USD","targetCurrency":"GBP","payOut":"BANK_TRANSFER","payIn":"VISA_CREDIT","price":{"priceSetId":556,"total":{"type":"TOTAL","label":"Total
+ fees","value":{"amount":4.51,"currency":"USD","label":"4.51 USD"}},"items":[{"type":"PAYIN","label":"fee","value":{"amount":3.39,"currency":"USD","label":"3.39
+ USD"}},{"type":"TRANSFERWISE","label":"Our fee","value":{"amount":1.12,"currency":"USD","label":"1.12
+ USD"}}],"priceDecisionReferenceId":"620f7926-59aa-425d-02a4-2c085e4d509b"},"fee":{"transferwise":1.12,"payIn":3.39,"discount":0,"total":4.51,"priceSetId":556,"partner":0.0},"disabled":false}],"notices":[],"transferFlowConfig":{"highAmount":{"showFeePercentage":false,"trackAsHighAmountSender":false,"showEducationStep":false,"offerPrefundingOption":false,"overLimitThroughCs":false,"overLimitThroughWiseAccount":false}},"rateTimestamp":"2025-08-07T09:29:42Z","targetAccountDetailsOwnedByProfile":false,"clientId":"transferwise-personal-tokens","sourceCurrency":"USD","targetCurrency":"GBP","createdTime":"2025-08-07T09:30:39Z","rateType":"FIXED","payOut":"BANK_TRANSFER","funding":"POST","rateExpirationTime":"2025-08-07T21:59:59Z","providedAmountType":"TARGET","targetAccount":701432364,"payInCountry":"DE","payOutCountry":"GB","user":13122309,"profile":,"rate":0.747885,"guaranteedTargetAmount":false,"status":"PENDING","expirationTime":"2025-08-07T10:00:39Z","id":"f21f78f3-c5f8-47c3-8dac-971f09699968","type":"REGULAR"}'
+ recorded_at: Thu, 07 Aug 2025 09:30:40 GMT
+- request:
+ method: post
+ uri: https://api.sandbox.transferwise.tech/v1/transfers
+ body:
+ encoding: UTF-8
+ string: '{"targetAccount":"701432364","quoteUuid":"f21f78f3-c5f8-47c3-8dac-971f09699968","customerTransactionId":"b11095fe-ccc7-47bf-984b-5285f3aa63d1","details":{"transferPurpose":"verification.transfers.purpose.pay.other","sourceOfFunds":"verification.source.of.funds.other","reference":"PMT"}}'
+ headers:
+ Authorization:
+ - Bearer 8ead1034-8730-46a5-8750-7ce0482ef0bf
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Thu, 07 Aug 2025 09:30:41 GMT
+ Content-Type:
+ - application/json;charset=UTF-8
+ Transfer-Encoding:
+ - chunked
+ Connection:
+ - keep-alive
+ Cf-Ray:
+ - 96b5c1b17a519a95-NAG
+ X-Content-Type-Options:
+ - nosniff
+ X-Xss-Protection:
+ - '0'
+ X-Frame-Options:
+ - DENY
+ Cache-Control:
+ - no-cache, no-store, max-age=0, must-revalidate
+ Expires:
+ - '0'
+ Pragma:
+ - no-cache
+ X-Trace-Id:
+ - f5d266b5cc33eea36420f799169997ea
+ Route:
+ - public_transfer_post_transfer
+ X-Envoy-Upstream-Service-Time:
+ - '857'
+ X-Envoy-Attempt-Count:
+ - '1'
+ Vary:
+ - origin,access-control-request-method,access-control-request-headers,accept-encoding
+ Cf-Cache-Status:
+ - DYNAMIC
+ Set-Cookie:
+ - __cf_bm=deQQ2i4rbIrNuescRxDmQG0J1xruOmtWq9aNN.lfjMA-1754559041-1.0.1.1-pwZnmhz_8GT6ohonoY11wVRnZdh.LZFtwns2Rcw.U71Cjw9cFLSFTi19PJv53ntyp21G9JOXsiCT8ek9zAdZwEGH3opfp7h4gA57B4JKQTs;
+ path=/; expires=Thu, 07-Aug-25 10:00:41 GMT; domain=.transferwise.tech; HttpOnly;
+ Secure; SameSite=None
+ Server:
+ - cloudflare
+ Alt-Svc:
+ - h3=":443"; ma=86400
+ body:
+ encoding: ASCII-8BIT
+ string: '{"id":55309977,"user":13122309,"targetAccount":701432364,"sourceAccount":null,"quote":null,"quoteUuid":"f21f78f3-c5f8-47c3-8dac-971f09699968","status":"incoming_payment_waiting","reference":"PMT","rate":0.747885,"created":"2025-08-07
+ 09:30:41","business":null,"transferRequest":null,"details":{"reference":"PMT"},"hasActiveIssues":false,"sourceCurrency":"USD","sourceValue":60.00,"targetCurrency":"GBP","targetValue":44.87,"customerTransactionId":"b11095fe-ccc7-47bf-984b-5285f3aa63d1"}'
+ recorded_at: Thu, 07 Aug 2025 09:30:41 GMT
+recorded_with: VCR 6.3.1
diff --git a/backend/spec/mailers/user_mailer_spec.rb b/backend/spec/mailers/user_mailer_spec.rb
index 4b51fcc966..9700d6a728 100644
--- a/backend/spec/mailers/user_mailer_spec.rb
+++ b/backend/spec/mailers/user_mailer_spec.rb
@@ -15,10 +15,9 @@
end
it "renders the body" do
- expect(mail.body.encoded).to include("Your verification code is:")
+ expect(mail.body.encoded).to include("Your verification code is")
expect(mail.body.encoded).to include(user.otp_code)
expect(mail.body.encoded).to include("This code will expire in 10 minutes")
- expect(mail.body.encoded).to include(ApplicationMailer::SUPPORT_EMAIL)
end
it "includes the correct OTP code" do
diff --git a/backend/spec/services/create_or_update_invoice_service_spec.rb b/backend/spec/services/create_or_update_invoice_service_spec.rb
index 867b063bf8..e3dddda8bf 100644
--- a/backend/spec/services/create_or_update_invoice_service_spec.rb
+++ b/backend/spec/services/create_or_update_invoice_service_spec.rb
@@ -146,23 +146,6 @@
end.to change { user.invoices.count }.by(1)
end
- it "does not apply an equity split if the feature is not enabled" do
- contractor.update!(equity_percentage: 60)
- company.update!(equity_enabled: false)
-
- expect do
- result = invoice_service.process
- expect(result[:success]).to be(true)
- invoice = result[:invoice]
- expect(invoice.total_amount_in_usd_cents).to eq(expected_total_amount_in_cents)
- expect(invoice.equity_percentage).to eq(0)
- expect(invoice.cash_amount_in_cents).to eq(expected_total_amount_in_cents)
- expect(invoice.flexile_fee_cents).to eq(50 + (1.5 * expected_total_amount_in_cents / 100).round)
- expect(invoice.equity_amount_in_cents).to eq(0)
- expect(invoice.equity_amount_in_options).to eq(0)
- expect(invoice.notes).to eq("Tax ID: 123efjo32r")
- end.to change { user.invoices.count }.by(1)
- end
it "fails to create an invoice if an active grant is missing, company does not have a share price, and the contractor has an equity percentage" do
contractor.update!(equity_percentage: 20)
diff --git a/backend/spec/services/pay_investor_dividends_spec.rb b/backend/spec/services/pay_investor_dividends_spec.rb
index e1627ec8fa..2cb06e5514 100644
--- a/backend/spec/services/pay_investor_dividends_spec.rb
+++ b/backend/spec/services/pay_investor_dividends_spec.rb
@@ -72,13 +72,6 @@
end.to change(DividendPayment, :count).by(0)
end
- it "raises an exception if the company does not have access to the dividends feature" do
- company.update!(equity_enabled: false)
-
- expect do
- described_class.new(company_investor, dividends).process
- end.to raise_error("Feature unsupported for company #{company.id}")
- end
it "raises an exception if Flexile does not have sufficient balance to pay for the dividend" do
allow(Wise::AccountBalance).to receive(:has_sufficient_flexile_balance?).and_return(false)
diff --git a/backend/spec/services/pay_investor_equity_buybacks_spec.rb b/backend/spec/services/pay_investor_equity_buybacks_spec.rb
index 4a28c9ff6d..cf60234692 100644
--- a/backend/spec/services/pay_investor_equity_buybacks_spec.rb
+++ b/backend/spec/services/pay_investor_equity_buybacks_spec.rb
@@ -74,13 +74,6 @@
end.to change(EquityBuybackPayment, :count).by(0)
end
- it "raises an exception if the company does not have access to the feature" do
- company.update!(equity_enabled: false)
-
- expect do
- described_class.new(company_investor, equity_buybacks).process
- end.to raise_error("Feature unsupported for company #{company.id}")
- end
it "raises an exception if Flexile does not have sufficient balance to pay for the equity buyback" do
allow(Wise::AccountBalance).to receive(:has_sufficient_flexile_balance?).and_return(false)
diff --git a/backend/spec/services/pay_invoice_spec.rb b/backend/spec/services/pay_invoice_spec.rb
index ae6ffeef5f..6324583a83 100644
--- a/backend/spec/services/pay_invoice_spec.rb
+++ b/backend/spec/services/pay_invoice_spec.rb
@@ -203,7 +203,7 @@ def setup_flexile_insufficient_balance
expect(invoice.reload.status).to eq(Invoice::FAILED)
end
- it "marks the invoice and payment as failed if creating a quote fails" do
+ it "marks the invoice and payment as failed if creating a quote fails and sends a notification" do
allow_any_instance_of(Wise::PayoutApi).to receive(:create_quote) do
{ "error" => "some error" }
end
@@ -213,6 +213,11 @@ def setup_flexile_insufficient_balance
described_class.new(invoice.id).process
end.to raise_error(described_class::WiseError) { |error| expect(error.message).to eq "Creating quote failed for payment #{Payment.last.id}" }
.and change { invoice.payments.count }.by(1)
+ .and have_enqueued_mail(CompanyWorkerMailer, :payment_failed).with { |payment_id, amount, currency|
+ expect(payment_id).to eq(Payment.last.id)
+ expect(amount).to be_a(Float)
+ expect(currency).to eq(invoice.user.bank_account.currency)
+ }
payment = Payment.last
expect(payment.processor_uuid).to be_present
@@ -222,7 +227,7 @@ def setup_flexile_insufficient_balance
expect(invoice.reload.status).to eq(Invoice::FAILED)
end
- it "marks the invoice and payment as failed if creating a transfer fails" do
+ it "marks the invoice and payment as failed if creating a transfer fails and sends a notification" do
allow_any_instance_of(Wise::PayoutApi).to receive(:create_transfer) do
{ "error" => "some error" }
end
@@ -232,6 +237,11 @@ def setup_flexile_insufficient_balance
described_class.new(invoice.id).process
end.to raise_error(described_class::WiseError) { |error| expect(error.message).to eq "Creating transfer failed for payment #{Payment.last.id}" }
.and change { invoice.payments.count }.by(1)
+ .and have_enqueued_mail(CompanyWorkerMailer, :payment_failed).with { |payment_id, amount, currency|
+ expect(payment_id).to eq(Payment.last.id)
+ expect(amount).to be_a(Float)
+ expect(currency).to eq(invoice.user.bank_account.currency)
+ }
payment = Payment.last
expect(payment.processor_uuid).to be_present
@@ -241,7 +251,7 @@ def setup_flexile_insufficient_balance
expect(invoice.reload.status).to eq(Invoice::FAILED)
end
- it "marks the invoice and payment as failed if funding fails" do
+ it "marks the invoice and payment as failed if funding fails and sends a notification" do
allow_any_instance_of(Wise::PayoutApi).to receive(:fund_transfer) do
{
"type" => "BALANCE",
@@ -257,6 +267,11 @@ def setup_flexile_insufficient_balance
described_class.new(invoice.id).process
end.to raise_error(described_class::WiseError) { |error| expect(error.message).to eq "Funding transfer failed for payment #{Payment.last.id}" }
.and change { invoice.payments.count }.by(1)
+ .and have_enqueued_mail(CompanyWorkerMailer, :payment_failed).with { |payment_id, amount, currency|
+ expect(payment_id).to eq(Payment.last.id)
+ expect(amount).to be_a(Float)
+ expect(currency).to eq(invoice.user.bank_account.currency)
+ }
payment = Payment.last
expect(payment.processor_uuid).to be_present
diff --git a/backend/spec/sidekiq/wise_transfer_update_job_spec.rb b/backend/spec/sidekiq/wise_transfer_update_job_spec.rb
new file mode 100644
index 0000000000..828f8ad7a5
--- /dev/null
+++ b/backend/spec/sidekiq/wise_transfer_update_job_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+RSpec.describe WiseTransferUpdateJob do
+ describe "#perform" do
+ let(:company) { create(:company) }
+ let(:user) { create(:user) }
+ let(:company_contractor) { create(:company_contractor, company: company, user: user) }
+ let(:invoice) { create(:invoice, company: company, user: user) }
+ let(:wise_credential) { create(:wise_credential) }
+ let(:payment) { create(:payment, invoice: invoice, wise_credential: wise_credential, wise_transfer_id: "transfer_123") }
+ let(:transfer_id) { payment.wise_transfer_id }
+ let(:payout_api) { instance_double(Wise::PayoutApi) }
+
+ before do
+ allow(Wise::PayoutApi).to receive(:new).and_return(payout_api)
+ allow(payout_api).to receive(:get_transfer).and_return({ "sourceValue" => 100.0, "targetValue" => 95.0, "targetCurrency" => "USD" })
+ allow(payout_api).to receive(:delivery_estimate).and_return({ "estimatedDeliveryDate" => "2025-08-10T12:00:00Z" })
+ end
+
+ context "when payment fails" do
+ let(:webhook_params) do
+ {
+ "data" => {
+ "resource" => {
+ "id" => transfer_id,
+ "profile_id" => wise_credential.profile_id,
+ },
+ "current_state" => Payments::Wise::CANCELLED,
+ "occurred_at" => "2025-08-07T10:00:00Z",
+ },
+ }
+ end
+
+ it "updates the payment status and sends a notification" do
+ expect do
+ described_class.new.perform(webhook_params)
+ end.to have_enqueued_mail(CompanyWorkerMailer, :payment_failed).with { |payment_id, amount, currency|
+ expect(payment_id).to eq(payment.id)
+ expect(amount).to eq(95.0)
+ expect(currency).to eq("USD")
+ }
+
+ payment.reload
+ invoice.reload
+
+ expect(payment.status).to eq(Payment::FAILED)
+ expect(payment.wise_transfer_status).to eq(Payments::Wise::CANCELLED)
+ expect(invoice.status).to eq(Invoice::FAILED)
+ end
+ end
+
+ context "when payment succeeds" do
+ let(:webhook_params) do
+ {
+ "data" => {
+ "resource" => {
+ "id" => transfer_id,
+ "profile_id" => wise_credential.profile_id,
+ },
+ "current_state" => Payments::Wise::OUTGOING_PAYMENT_SENT,
+ "occurred_at" => "2025-08-07T10:00:00Z",
+ },
+ }
+ end
+
+ it "updates the payment status and does not send a failure notification" do
+ expect do
+ described_class.new.perform(webhook_params)
+ end.not_to have_enqueued_mail(CompanyWorkerMailer, :payment_failed)
+
+ payment.reload
+ invoice.reload
+
+ expect(payment.status).to eq(Payment::SUCCEEDED)
+ expect(payment.wise_transfer_status).to eq(Payments::Wise::OUTGOING_PAYMENT_SENT)
+ expect(invoice.status).to eq(Invoice::PAID)
+ end
+ end
+ end
+end
diff --git a/backend/test/mailers/previews/company_worker_mailer_preview.rb b/backend/test/mailers/previews/company_worker_mailer_preview.rb
index 99f1032d36..14c8bcb915 100644
--- a/backend/test/mailers/previews/company_worker_mailer_preview.rb
+++ b/backend/test/mailers/previews/company_worker_mailer_preview.rb
@@ -32,6 +32,14 @@ def payment_failed_reenter_bank_details
CompanyWorkerMailer.payment_failed_reenter_bank_details(Payment.last.id, amount, currency)
end
+ def payment_failed
+ payment = Payment.last.invoice
+ rate = Wise::PayoutApi.new.get_exchange_rate(target_currency: payment.user.bank_account.currency).first["rate"]
+ amount = payment.cash_amount_in_usd * rate
+ currency = payment.user.bank_account.currency
+ CompanyWorkerMailer.payment_failed(Payment.last.id, amount, currency)
+ end
+
def invoice_approved
CompanyWorkerMailer.invoice_approved(invoice_id: Invoice.alive.where(equity_percentage: 0).last.id)
end
diff --git a/e2e/helpers/auth.ts b/e2e/helpers/auth.ts
index c84232dbee..9a4f5fc4e9 100644
--- a/e2e/helpers/auth.ts
+++ b/e2e/helpers/auth.ts
@@ -16,7 +16,10 @@ export const login = async (page: Page, user: typeof users.$inferSelect) => {
await page.getByLabel("Verification code").waitFor();
// Use test OTP code - backend should accept this in test environment
- await page.getByLabel("Verification code").fill(TEST_OTP_CODE);
+ // The InputOTP component uses a hidden input for actual input
+ // Type into the OTP input container to trigger the input
+ await page.locator('[data-slot="input-otp"]').fill(TEST_OTP_CODE);
+
await page.getByRole("button", { name: "Continue" }).click();
// Wait for successful redirect
@@ -46,9 +49,11 @@ export const signup = async (page: Page, email: string) => {
// Wait for OTP step and enter verification code
await page.getByLabel("Verification code").waitFor();
- await page.getByLabel("Verification code").fill(TEST_OTP_CODE);
- await page.getByRole("button", { name: "Continue" }).click();
- // Wait for successful redirect to onboarding or dashboard
+ // The InputOTP component uses a hidden input for actual input
+ // Type into the OTP input container to trigger the input
+ await page.locator('[data-slot="input-otp"]').fill(TEST_OTP_CODE);
+
+ await page.getByRole("button", { name: "Continue" }).click(); // Wait for successful redirect to onboarding or dashboard
await page.waitForURL(/^(?!.*\/(signup|login)$).*/u);
};
diff --git a/e2e/tests/company/administrator/onboarding/signup.spec.ts b/e2e/tests/company/administrator/onboarding/signup.spec.ts
index 9b2b6b308f..eba1d3089f 100644
--- a/e2e/tests/company/administrator/onboarding/signup.spec.ts
+++ b/e2e/tests/company/administrator/onboarding/signup.spec.ts
@@ -27,10 +27,11 @@ test.describe("Company administrator signup", () => {
await page.getByRole("button", { name: "Sign up" }).click();
// Wait for OTP step and enter verification code
- await page.getByLabel("Verification code").waitFor();
- await page.getByLabel("Verification code").fill("000000"); // Test OTP code
- await page.getByRole("button", { name: "Continue" }).click();
+ // The form should auto-submit when all 6 digits are entered
+ const otpCode = "000000";
+ await page.locator('[data-slot="input-otp"]').fill(otpCode);
+ // No need to click the button as it should auto-submit
// Wait for redirect to dashboard
await page.waitForURL(/.*\/invoices.*/u);
diff --git a/e2e/tests/company/contractor/contractor-invite-link.spec.ts b/e2e/tests/company/contractor/contractor-invite-link.spec.ts
index 45d1b28e68..c465052e4e 100644
--- a/e2e/tests/company/contractor/contractor-invite-link.spec.ts
+++ b/e2e/tests/company/contractor/contractor-invite-link.spec.ts
@@ -46,10 +46,44 @@ test.describe("Contractor Invite Link Joining flow", () => {
test("invite link flow for unauthenticated user", async ({ page, context }) => {
await page.goto(`/invite/${inviteLink?.token}`);
await expect(page).toHaveURL(/signup/iu);
+ expect(page.url()).toContain(`invitation_token=${inviteLink?.token}`);
const cookies = await context.cookies();
const invitationCookie = cookies.find((c) => c.name === "invitation_token");
expect(inviteLink?.token).toContain(invitationCookie?.value);
+
+ const email = "contractor-signup+e2e@example.com";
+
+ // Clean up any existing user with this email
+ await db.delete(users).where(eq(users.email, email));
+
+ // Enter email and request OTP
+ await page.getByLabel("Work email").fill(email);
+ await page.getByRole("button", { name: "Sign up" }).click();
+
+ // Wait for OTP step and enter verification code
+ await page.getByLabel("Verification code").waitFor();
+ await page.getByLabel("Verification code").fill("000000"); // Test OTP code
+ await page.getByRole("button", { name: "Continue" }).click();
+
+ await expect(page).toHaveURL(/documents/iu);
+
+ const contractor = await db.query.users.findFirst({
+ where: eq(users.email, email),
+ });
+
+ const contractorId = contractor?.id;
+ expect(contractorId).toBeDefined();
+
+ if (contractorId) {
+ const createdCompayContractor = await db.query.companyContractors.findFirst({
+ where: and(eq(companyContractors.companyId, company.id), eq(companyContractors.userId, contractorId)),
+ });
+
+ expect(createdCompayContractor).toBeDefined();
+ expect(createdCompayContractor?.role).toBe(null);
+ expect(createdCompayContractor?.contractSignedElsewhere).toBe(true);
+ }
});
test("invite link flow for authenticated user", async ({ page }) => {
diff --git a/e2e/tests/company/invoices/list.spec.ts b/e2e/tests/company/invoices/list.spec.ts
index f934f4abb6..4e8edbe343 100644
--- a/e2e/tests/company/invoices/list.spec.ts
+++ b/e2e/tests/company/invoices/list.spec.ts
@@ -416,18 +416,19 @@ test.describe("Invoices contractor flow", () => {
await receivedInvoiceRow.click({ button: "right" });
await expect(page.getByRole("menuitem").filter({ hasText: "Delete" })).toBeVisible();
- await page.click("body");
+ await page.keyboard.press("Escape");
const paidInvoiceRow = page.getByRole("row").getByText("Paid");
await paidInvoiceRow.click({ button: "right" });
await expect(page.getByRole("menuitem").filter({ hasText: "Delete" })).not.toBeVisible();
- await page.click("body");
+ await page.keyboard.press("Escape");
await expect(page.locator("tbody tr")).toHaveCount(3);
const deletableInvoiceRow = page.getByRole("row").getByText("Awaiting approval").first();
await deletableInvoiceRow.click({ button: "right" });
+ await expect(page.getByRole("menuitem", { name: "Delete" })).toBeVisible();
await page.getByRole("menuitem", { name: "Delete" }).click();
await page.getByRole("dialog").waitFor();
await page.getByRole("button", { name: "Delete" }).click();
diff --git a/e2e/tests/company/updates/company/create.spec.ts b/e2e/tests/company/updates/company/create.spec.ts
index 8838b7ac7d..156a4009cf 100644
--- a/e2e/tests/company/updates/company/create.spec.ts
+++ b/e2e/tests/company/updates/company/create.spec.ts
@@ -21,9 +21,14 @@ test.describe("company update creation", () => {
await companyInvestorsFactory.create({ companyId: company.id });
});
- async function fillForm(page: Page, title: string, body: string) {
- await page.getByLabel("Title").fill(title);
- await page.locator('[contenteditable="true"]').fill(body);
+ async function fillFormInModal(page: Page, title: string, body: string, modalTitle: string) {
+ await withinModal(
+ async (modal) => {
+ await modal.getByLabel("Title").fill(title);
+ await modal.locator('[contenteditable="true"]').fill(body);
+ },
+ { page, title: modalTitle },
+ );
}
test("allows publishing company update", async ({ page }) => {
@@ -31,15 +36,24 @@ test.describe("company update creation", () => {
const content = "This will be published";
await login(page, adminUser);
- await page.goto("/updates/company/new");
+ await page.goto("/updates/company");
+
+ await page.getByRole("button", { name: "New update" }).click();
+ await expect(page.getByRole("dialog", { name: "New company update" })).toBeVisible();
- await fillForm(page, title, content);
- await page.getByRole("button", { name: "Publish" }).click();
+ await fillFormInModal(page, title, content, "New company update");
+
+ await withinModal(
+ async (modal) => {
+ await modal.getByRole("button", { name: "Publish" }).click();
+ },
+ { page, title: "New company update" },
+ );
await expect(page.getByRole("dialog", { name: "Publish update?" })).toBeVisible();
await page.getByRole("button", { name: "Yes, publish" }).click();
- await page.waitForURL("/updates/company");
+ await expect(page.getByRole("dialog")).not.toBeVisible();
await expect(page.getByRole("row").filter({ hasText: title }).filter({ hasText: "Sent" })).toBeVisible();
const updates = await db.query.companyUpdates.findMany({
@@ -54,11 +68,19 @@ test.describe("company update creation", () => {
const content = "Test content";
await login(page, adminUser);
- await page.goto("/updates/company/new");
+ await page.goto("/updates/company");
+
+ await page.getByRole("button", { name: "New update" }).click();
+ await expect(page.getByRole("dialog", { name: "New company update" })).toBeVisible();
- await fillForm(page, title, content);
+ await fillFormInModal(page, title, content, "New company update");
- await page.getByRole("button", { name: "Preview" }).click();
+ await withinModal(
+ async (modal) => {
+ await modal.getByRole("button", { name: "Preview" }).click();
+ },
+ { page, title: "New company update" },
+ );
await withinModal(
async (modal) => {
@@ -77,15 +99,37 @@ test.describe("company update creation", () => {
test("prevents submission with validation errors", async ({ page }) => {
await login(page, adminUser);
- await page.goto("/updates/company/new");
+ await page.goto("/updates/company");
- await page.getByRole("button", { name: "Preview" }).click();
- await expect(page.locator('[data-slot="form-message"]').first()).toBeVisible();
- await expect(page.getByRole("dialog")).not.toBeVisible();
+ await page.getByRole("button", { name: "New update" }).click();
+ await expect(page.getByRole("dialog", { name: "New company update" })).toBeVisible();
- await page.getByRole("button", { name: "Publish" }).click();
- await expect(page.locator('[data-slot="form-message"]').first()).toBeVisible();
- await expect(page.getByRole("dialog")).not.toBeVisible();
+ await withinModal(
+ async (modal) => {
+ await modal.getByLabel("Title").fill("Important update");
+ },
+ { page, title: "New company update" },
+ );
+
+ await withinModal(
+ async (modal) => {
+ await modal.getByRole("button", { name: "Preview" }).click();
+ await expect(modal.locator('[data-slot="form-message"]').first()).toBeVisible();
+ },
+ { page, title: "New company update" },
+ );
+
+ await expect(page.getByRole("dialog", { name: "Previewing: Important update" })).not.toBeVisible();
+
+ await withinModal(
+ async (modal) => {
+ await modal.getByRole("button", { name: "Publish" }).click();
+ await expect(modal.locator('[data-slot="form-message"]').first()).toBeVisible();
+ },
+ { page, title: "New company update" },
+ );
+
+ await expect(page.getByRole("dialog", { name: "Publish update?" })).not.toBeVisible();
const updates = await db.query.companyUpdates.findMany({
where: eq(companyUpdates.companyId, company.id),
@@ -101,16 +145,24 @@ test.describe("company update creation", () => {
});
await login(page, adminUser);
- await page.goto(`/updates/company/${companyUpdate.externalId}/edit`);
+ await page.goto("/updates/company");
+
+ await page.getByRole("row").filter({ hasText: "Original Title" }).click();
+ await expect(page.getByRole("dialog", { name: "Edit company update" })).toBeVisible();
- await page.getByLabel("Title").clear();
- await page.getByLabel("Title").fill("Updated Title");
+ await withinModal(
+ async (modal) => {
+ await modal.getByLabel("Title").clear();
+ await modal.getByLabel("Title").fill("Updated Title");
+ await modal.getByRole("button", { name: "Update" }).click();
+ },
+ { page, title: "Edit company update" },
+ );
- await page.getByRole("button", { name: "Update" }).click();
await expect(page.getByRole("dialog", { name: "Publish update?" })).toBeVisible();
await page.getByRole("button", { name: "Yes, update" }).click();
- await page.waitForURL("/updates/company");
+ await expect(page.getByRole("dialog")).not.toBeVisible();
await expect(page.getByRole("row").filter({ hasText: "Updated Title" }).filter({ hasText: "Sent" })).toBeVisible();
const updatedRecord = await db.query.companyUpdates.findFirst({
diff --git a/e2e/tests/company/updates/company/youtube-embeds.spec.ts b/e2e/tests/company/updates/company/youtube-embeds.spec.ts
index c4f7dfa591..7ca55f4ec8 100644
--- a/e2e/tests/company/updates/company/youtube-embeds.spec.ts
+++ b/e2e/tests/company/updates/company/youtube-embeds.spec.ts
@@ -129,16 +129,24 @@ test.describe("Company Updates - YouTube Embeds", () => {
test("should allow creating company update with YouTube URL", async ({ page }) => {
await login(page, adminUser);
- await page.goto("/updates/company/new");
+ await page.goto("/updates/company");
- await expect(page.getByLabel("Title")).toBeVisible();
- await expect(page.getByLabel("Video URL (optional)")).toBeVisible();
+ await page.getByRole("button", { name: "New update" }).click();
+ await expect(page.getByRole("dialog", { name: "New company update" })).toBeVisible();
- await page.getByLabel("Title").fill("Test Update with YouTube Video");
- await page.getByLabel("Video URL (optional)").fill(ANTIWORK_VIDEO.fullUrl);
+ await withinModal(
+ async (modal) => {
+ await expect(modal.getByLabel("Title")).toBeVisible();
+ await expect(modal.getByLabel("Video URL (optional)")).toBeVisible();
+
+ await modal.getByLabel("Title").fill("Test Update with YouTube Video");
+ await modal.getByLabel("Video URL (optional)").fill(ANTIWORK_VIDEO.fullUrl);
- await expect(page.getByLabel("Title")).toHaveValue("Test Update with YouTube Video");
- await expect(page.getByLabel("Video URL (optional)")).toHaveValue(ANTIWORK_VIDEO.fullUrl);
+ await expect(modal.getByLabel("Title")).toHaveValue("Test Update with YouTube Video");
+ await expect(modal.getByLabel("Video URL (optional)")).toHaveValue(ANTIWORK_VIDEO.fullUrl);
+ },
+ { page, title: "New company update" },
+ );
});
test("should handle YouTube URLs with additional parameters", async ({ page }) => {
diff --git a/e2e/tests/login.spec.ts b/e2e/tests/login.spec.ts
index 20f61feab9..af54cb0c81 100644
--- a/e2e/tests/login.spec.ts
+++ b/e2e/tests/login.spec.ts
@@ -12,8 +12,15 @@ test("login", async ({ page }) => {
await page.getByLabel("Work email").fill(email);
await page.getByRole("button", { name: "Log in", exact: true }).click();
- await page.getByLabel("Verification code").fill("000000");
- await page.getByRole("button", { name: "Continue", exact: true }).click();
+
+ // Fill the OTP code using the InputOTP component's hidden input
+ // The form should auto-submit when all 6 digits are entered
+ const otpCode = "000000";
+ await page.locator('[data-slot="input-otp"]').fill(otpCode);
+
+ // No need to click the button as it should auto-submit
+ // Wait for navigation to complete after auto-submit
+ await page.waitForURL(/.*\/invoices.*/u);
await expect(page.getByRole("heading", { name: "Invoices" })).toBeVisible();
@@ -35,9 +42,13 @@ test("login with redirect_url", async ({ page }) => {
await page.getByLabel("Work email").fill(email);
await page.getByRole("button", { name: "Log in", exact: true }).click();
- await page.getByLabel("Verification code").fill("000000");
- await page.getByRole("button", { name: "Continue", exact: true }).click();
+ // Fill the OTP code using the InputOTP component's hidden input
+ // The form should auto-submit when all 6 digits are entered
+ const otpCode = "000000";
+ await page.locator('[data-slot="input-otp"]').fill(otpCode);
+
+ // No need to click the button as it should auto-submit
await page.waitForLoadState("networkidle");
await expect(page.getByRole("heading", { name: "People" })).toBeVisible();
diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx
index 20779b524f..7d0c5d4fbd 100644
--- a/frontend/app/(auth)/login/page.tsx
+++ b/frontend/app/(auth)/login/page.tsx
@@ -1,17 +1,19 @@
"use client";
import Image from "next/image";
import Link from "next/link";
-import { Suspense } from "react";
+import { Suspense, useRef } from "react";
import { AuthAlerts } from "@/components/auth/AuthAlerts";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
+import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { useAuthApi } from "@/hooks/useAuthApi";
import { useOtpFlowState } from "@/hooks/useOtpFlowState";
import logo from "@/public/logo-icon.svg";
function LoginContent() {
+ const formRef = useRef(null);
const [state, actions] = useOtpFlowState();
const { handleSendOtp, handleAuthenticate } = useAuthApi(
{
@@ -78,23 +80,40 @@ function LoginContent() {
void handleAuthenticate(e);
}}
className="space-y-4"
+ ref={formRef}
>
-
+
Verification code
- actions.setOtp(e.target.value)}
maxLength={6}
- required
+ value={state.otp}
+ onChange={(value) => {
+ actions.setOtp(value);
+ if (value.length === 6 && !state.loading) {
+ setTimeout(() => formRef.current?.requestSubmit(), 100);
+ }
+ }}
disabled={state.loading}
- />
+ autoFocus
+ required
+ >
+
+
+
+
+
+
+
+
+
+
+
+
-
+
{state.loading ? "Verifying..." : "Continue"}
diff --git a/frontend/app/(auth)/signup/[[...rest]]/page.tsx b/frontend/app/(auth)/signup/[[...rest]]/page.tsx
index 92f78a112f..d5c3c8cd9f 100644
--- a/frontend/app/(auth)/signup/[[...rest]]/page.tsx
+++ b/frontend/app/(auth)/signup/[[...rest]]/page.tsx
@@ -2,11 +2,12 @@
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
-import { Suspense } from "react";
+import { Suspense, useRef } from "react";
import { AuthAlerts } from "@/components/auth/AuthAlerts";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
+import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp";
import { Label } from "@/components/ui/label";
import { useAuthApi } from "@/hooks/useAuthApi";
import { useOtpFlowState } from "@/hooks/useOtpFlowState";
@@ -15,6 +16,7 @@ import logo from "@/public/logo-icon.svg";
function SignUpContent() {
const searchParams = useSearchParams();
const invitationToken = searchParams.get("invitation_token");
+ const formRef = useRef(null);
const [state, actions] = useOtpFlowState();
const { handleSendOtp, handleAuthenticate } = useAuthApi(
@@ -85,23 +87,40 @@ function SignUpContent() {
void handleAuthenticate(e);
}}
className="space-y-4"
+ ref={formRef}
>
-
+
Verification code
- actions.setOtp(e.target.value)}
maxLength={6}
- required
+ value={state.otp}
+ onChange={(value) => {
+ actions.setOtp(value);
+ if (value.length === 6 && !state.loading) {
+ setTimeout(() => formRef.current?.requestSubmit(), 100);
+ }
+ }}
disabled={state.loading}
- />
+ autoFocus
+ required
+ >
+
+
+
+
+
+
+
+
+
+
+
+
-
+
{state.loading ? "Creating account..." : "Continue"}
diff --git a/frontend/app/(dashboard)/layout.tsx b/frontend/app/(dashboard)/layout.tsx
index b030041ccd..9b3bb8ecee 100644
--- a/frontend/app/(dashboard)/layout.tsx
+++ b/frontend/app/(dashboard)/layout.tsx
@@ -154,19 +154,10 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
) : null}
- {company.checklistItems.length > 0 ? (
-
-
-
-
-
-
-
- ) : null}
-
+ {company.checklistItems.length > 0 ? : null}
{canShowTryEquity && showTryEquity ? (
@@ -179,7 +170,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
tabIndex={0}
>
-
+
Try equity
{hovered ? (
diff --git a/frontend/app/(dashboard)/updates/company/CompanyUpdateModal.tsx b/frontend/app/(dashboard)/updates/company/CompanyUpdateModal.tsx
new file mode 100644
index 0000000000..ee472478c0
--- /dev/null
+++ b/frontend/app/(dashboard)/updates/company/CompanyUpdateModal.tsx
@@ -0,0 +1,263 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useMutation } from "@tanstack/react-query";
+import { Users } from "lucide-react";
+import React, { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+import ViewUpdateDialog from "@/app/(dashboard)/updates/company/ViewUpdateDialog";
+import MutationButton, { MutationStatusButton } from "@/components/MutationButton";
+import { Editor as RichTextEditor } from "@/components/RichText";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { useCurrentCompany } from "@/global";
+import { trpc } from "@/trpc/client";
+import { pluralize } from "@/utils/pluralize";
+
+const formSchema = z.object({
+ title: z.string().trim().min(1, "This field is required."),
+ body: z.string().regex(/>\w/u, "This field is required."),
+ videoUrl: z.string().nullable(),
+});
+
+interface CompanyUpdateModalProps {
+ open: boolean;
+ onClose: () => void;
+ updateId?: string;
+}
+
+const CompanyUpdateModal = ({ open, onClose, updateId }: CompanyUpdateModalProps) => {
+ const company = useCurrentCompany();
+ const trpcUtils = trpc.useUtils();
+
+ const { data: update, isLoading } = trpc.companyUpdates.get.useQuery(
+ { companyId: company.id, id: updateId ?? "" },
+ { enabled: !!updateId && open },
+ );
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ title: update?.title ?? "",
+ body: update?.body ?? "",
+ videoUrl: update?.videoUrl ?? "",
+ },
+ });
+
+ useEffect(() => {
+ if (update) {
+ form.reset({
+ title: update.title,
+ body: update.body,
+ videoUrl: update.videoUrl ?? "",
+ });
+ } else if (!updateId) {
+ form.reset({
+ title: "",
+ body: "",
+ videoUrl: "",
+ });
+ }
+ }, [update, updateId, form]);
+
+ const [publishModalOpen, setPublishModalOpen] = useState(false);
+ const [viewPreview, setViewPreview] = useState(false);
+ const [previewUpdateId, setPreviewUpdateId] = useState(null);
+
+ const recipientCount = (company.contractorCount ?? 0) + (company.investorCount ?? 0);
+
+ const createMutation = trpc.companyUpdates.create.useMutation();
+ const updateMutation = trpc.companyUpdates.update.useMutation();
+ const publishMutation = trpc.companyUpdates.publish.useMutation();
+
+ const saveMutation = useMutation({
+ mutationFn: async ({ values, preview }: { values: z.infer; preview: boolean }) => {
+ const data = {
+ companyId: company.id,
+ ...values,
+ videoUrl: values.videoUrl || null,
+ };
+ let id;
+ if (update) {
+ id = update.id;
+ await updateMutation.mutateAsync({ ...data, id });
+ } else if (previewUpdateId) {
+ id = previewUpdateId;
+ await updateMutation.mutateAsync({ ...data, id });
+ } else {
+ id = await createMutation.mutateAsync(data);
+ }
+ if (!preview && !update?.sentAt) await publishMutation.mutateAsync({ companyId: company.id, id });
+ void trpcUtils.companyUpdates.list.invalidate();
+ await trpcUtils.companyUpdates.get.invalidate({ companyId: company.id, id });
+ if (preview) {
+ setPreviewUpdateId(id);
+ setViewPreview(true);
+ } else {
+ handleClose();
+ }
+ },
+ });
+
+ const submit = form.handleSubmit(() => setPublishModalOpen(true));
+
+ const handleClose = () => {
+ setPublishModalOpen(false);
+ setViewPreview(false);
+ setPreviewUpdateId(null);
+ form.reset();
+ onClose();
+ };
+
+ return (
+ <>
+
+
+
+ {update ? "Edit company update" : "New company update"}
+
+
+ {isLoading ? (
+ Loading...
+ ) : (
+
+
+ )}
+
+
+
+ {update?.sentAt ? (
+ void submit()}>Update
+ ) : (
+ <>
+
+ void form.handleSubmit((values) => saveMutation.mutateAsync({ values, preview: true }))()
+ }
+ >
+ Preview
+
+ void submit()}>Publish
+ >
+ )}
+
+
+
+
+
+
+
+
+ Publish update?
+
+ {update?.sentAt ? (
+ Your update will be visible in Flexile. No new emails will be sent.
+ ) : (
+ Your update will be emailed to {recipientCount.toLocaleString()} stakeholders.
+ )}
+
+
+ setPublishModalOpen(false)}>
+ No, cancel
+
+
+ Yes, {update?.sentAt ? "update" : "publish"}
+
+
+
+
+
+
+ {viewPreview && previewUpdateId ? (
+ {
+ setViewPreview(false);
+ }}
+ />
+ ) : null}
+ >
+ );
+};
+
+export default CompanyUpdateModal;
diff --git a/frontend/app/(dashboard)/updates/company/Edit.tsx b/frontend/app/(dashboard)/updates/company/Edit.tsx
deleted file mode 100644
index f1859f264e..0000000000
--- a/frontend/app/(dashboard)/updates/company/Edit.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-"use client";
-
-import { EnvelopeIcon, UsersIcon } from "@heroicons/react/24/outline";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useMutation } from "@tanstack/react-query";
-import { FileScan } from "lucide-react";
-import { useParams, usePathname, useRouter } from "next/navigation";
-import React, { useEffect, useState } from "react";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import ViewUpdateDialog from "@/app/(dashboard)/updates/company/ViewUpdateDialog";
-import { DashboardHeader } from "@/components/DashboardHeader";
-import MutationButton, { MutationStatusButton } from "@/components/MutationButton";
-import { Editor as RichTextEditor } from "@/components/RichText";
-import { Button } from "@/components/ui/button";
-import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { useCurrentCompany } from "@/global";
-import type { RouterOutput } from "@/trpc";
-import { trpc } from "@/trpc/client";
-import { pluralize } from "@/utils/pluralize";
-
-const formSchema = z.object({
- title: z.string().trim().min(1, "This field is required."),
- body: z.string().regex(/>\w/u, "This field is required."),
- videoUrl: z.string().nullable(),
-});
-
-type CompanyUpdate = RouterOutput["companyUpdates"]["get"];
-const Edit = ({ update }: { update?: CompanyUpdate }) => {
- const { id } = useParams<{ id?: string }>();
- const pathname = usePathname();
- const company = useCurrentCompany();
- const router = useRouter();
- const trpcUtils = trpc.useUtils();
-
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- title: update?.title ?? "",
- body: update?.body ?? "",
- videoUrl: update?.videoUrl ?? "",
- },
- });
-
- const [modalOpen, setModalOpen] = useState(false);
- const navigatedFromNewPreview = sessionStorage.getItem("navigated-from-new-preview");
- const [viewPreview, setViewPreview] = useState(!!navigatedFromNewPreview);
-
- const recipientCount = (company.contractorCount ?? 0) + (company.investorCount ?? 0);
-
- const createMutation = trpc.companyUpdates.create.useMutation();
- const updateMutation = trpc.companyUpdates.update.useMutation();
- const publishMutation = trpc.companyUpdates.publish.useMutation();
- const saveMutation = useMutation({
- mutationFn: async ({ values, preview }: { values: z.infer; preview: boolean }) => {
- const data = {
- companyId: company.id,
- ...values,
- };
- let id;
- if (update) {
- id = update.id;
- await updateMutation.mutateAsync({ ...data, id });
- } else {
- id = await createMutation.mutateAsync(data);
- }
- if (!preview && !update?.sentAt) await publishMutation.mutateAsync({ companyId: company.id, id });
- void trpcUtils.companyUpdates.list.invalidate();
- if (preview) {
- if (pathname === "/updates/company/new") {
- sessionStorage.setItem("navigated-from-new-preview", "yes");
- router.replace(`/updates/company/${id}/edit`);
- } else {
- await trpcUtils.companyUpdates.get.invalidate({ companyId: company.id, id });
- setViewPreview(true);
- }
- } else {
- router.push(`/updates/company`);
- }
- },
- });
-
- const submit = form.handleSubmit(() => setModalOpen(true));
-
- useEffect(() => {
- if (navigatedFromNewPreview) {
- sessionStorage.removeItem("navigated-from-new-preview");
- }
- }, []);
-
- return (
- <>
-
- ) : (
- <>
-
- void form.handleSubmit((values) => saveMutation.mutateAsync({ values, preview: true }))()
- }
- >
-
- Preview
-
-
-
- Publish
-
- >
- )
- }
- />
-
-
- (
-
- Title
-
-
-
-
-
- )}
- />
-
- (
-
- Update
-
-
-
-
-
- )}
- />
- (
-
- Video URL (optional)
-
-
-
-
-
- )}
- />
-
-
-
Recipients ({recipientCount.toLocaleString()})
- {company.investorCount ? (
-
-
-
- {company.investorCount.toLocaleString()} {pluralize("investor", company.investorCount)}
-
-
- ) : null}
- {company.contractorCount ? (
-
-
-
- {company.contractorCount.toLocaleString()} active {pluralize("contractor", company.contractorCount)}
-
-
- ) : null}
-
-
-
-
-
- Publish update?
-
- {update?.sentAt ? (
- Your update will be visible in Flexile. No new emails will be sent.
- ) : (
- Your update will be emailed to {recipientCount.toLocaleString()} stakeholders.
- )}
-
-
- setModalOpen(false)}>
- No, cancel
-
-
- Yes, {update?.sentAt ? "update" : "publish"}
-
-
-
-
-
-
-
- {viewPreview && id ? (
-
{
- setViewPreview(false);
- }}
- />
- ) : null}
- >
- );
-};
-
-export default Edit;
diff --git a/frontend/app/(dashboard)/updates/company/[id]/edit/page.tsx b/frontend/app/(dashboard)/updates/company/[id]/edit/page.tsx
deleted file mode 100644
index 07b611aaab..0000000000
--- a/frontend/app/(dashboard)/updates/company/[id]/edit/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-"use client";
-import { useParams } from "next/navigation";
-import React from "react";
-import EditPage from "@/app/(dashboard)/updates/company/Edit";
-import { useCurrentCompany } from "@/global";
-import { trpc } from "@/trpc/client";
-
-export default function Edit() {
- const company = useCurrentCompany();
- const { id } = useParams<{ id: string }>();
- const [update] = trpc.companyUpdates.get.useSuspenseQuery({ companyId: company.id, id });
-
- return ;
-}
diff --git a/frontend/app/(dashboard)/updates/company/new/page.tsx b/frontend/app/(dashboard)/updates/company/new/page.tsx
deleted file mode 100644
index 6d53fc843b..0000000000
--- a/frontend/app/(dashboard)/updates/company/new/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-"use client";
-import React from "react";
-import EditPage from "@/app/(dashboard)/updates/company/Edit";
-
-export default function New() {
- return ;
-}
diff --git a/frontend/app/(dashboard)/updates/company/page.tsx b/frontend/app/(dashboard)/updates/company/page.tsx
index e72954d590..0357899a6c 100644
--- a/frontend/app/(dashboard)/updates/company/page.tsx
+++ b/frontend/app/(dashboard)/updates/company/page.tsx
@@ -1,8 +1,7 @@
"use client";
import { CircleCheck, Trash2 } from "lucide-react";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
import React, { useMemo, useState } from "react";
+import CompanyUpdateModal from "@/app/(dashboard)/updates/company/CompanyUpdateModal";
import ViewUpdateDialog from "@/app/(dashboard)/updates/company/ViewUpdateDialog";
import { DashboardHeader } from "@/components/DashboardHeader";
import DataTable, { createColumnHelper, useTable } from "@/components/DataTable";
@@ -22,28 +21,41 @@ const useData = () => {
return { updates: data.updates, isLoading };
};
+type UpdateListItem = ReturnType["updates"][number];
+
export default function CompanyUpdates() {
const user = useCurrentUser();
const { updates, isLoading } = useData();
+ const [showModal, setShowModal] = useState(false);
+ const [editingUpdateId, setEditingUpdateId] = useState(undefined);
+
+ const handleNewUpdate = () => {
+ setEditingUpdateId(undefined);
+ setShowModal(true);
+ };
+
+ const handleEditUpdate = (update: UpdateListItem) => {
+ setEditingUpdateId(update.id);
+ setShowModal(true);
+ };
+
+ const handleCloseModal = () => {
+ setShowModal(false);
+ setEditingUpdateId(undefined);
+ };
return (
<>
- New update
-
- ) : null
- }
+ headerActions={user.roles.administrator ? New update : null}
/>
{isLoading ? (
) : updates.length ? (
user.roles.administrator ? (
-
+
) : (
)
@@ -52,14 +64,19 @@ export default function CompanyUpdates() {
No updates to display.
)}
+
+
>
);
}
-const AdminList = () => {
+const AdminList = ({ onEditUpdate }: { onEditUpdate: (update: UpdateListItem) => void }) => {
const { updates } = useData();
const company = useCurrentCompany();
- const router = useRouter();
const trpcUtils = trpc.useUtils();
const [deletingUpdate, setDeletingUpdate] = useState(null);
@@ -78,9 +95,9 @@ const AdminList = () => {
columnHelper.accessor("title", {
header: "Title",
cell: (info) => (
-
+ onEditUpdate(info.row.original)} className="text-left no-underline hover:underline">
{info.getValue()}
-
+
),
}),
columnHelper.accessor((row) => (row.sentAt ? "Sent" : "Draft"), {
@@ -93,7 +110,10 @@ const AdminList = () => {
setDeletingUpdate(info.row.original.id)}
+ onClick={(e) => {
+ e.stopPropagation();
+ setDeletingUpdate(info.row.original.id);
+ }}
className="inline-flex cursor-pointer items-center border-none bg-transparent text-inherit underline hover:text-blue-600"
>
@@ -101,14 +121,14 @@ const AdminList = () => {
),
}),
],
- [],
+ [onEditUpdate],
);
const table = useTable({ columns, data: updates });
return (
<>
- router.push(`/updates/company/${row.id}/edit`)} />
+ onEditUpdate(row)} />
setDeletingUpdate(null)}>
diff --git a/frontend/app/settings/layout.tsx b/frontend/app/settings/layout.tsx
index 9442955b5f..76c4c6d87f 100644
--- a/frontend/app/settings/layout.tsx
+++ b/frontend/app/settings/layout.tsx
@@ -100,7 +100,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
- Back to app
+ Back to app
@@ -150,7 +150,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
- Back to app
+ Back to app
{children}
diff --git a/frontend/components/GettingStarted.tsx b/frontend/components/GettingStarted.tsx
index abab21dc8b..0ba5f5393f 100644
--- a/frontend/components/GettingStarted.tsx
+++ b/frontend/components/GettingStarted.tsx
@@ -80,14 +80,14 @@ export const GettingStarted = () => {
}
return (
-
+
setStatus(expanded ? "expanded" : "collapsed")}
className="flex h-full flex-col-reverse"
>
-
+
{status === "completed" ? (
@@ -96,10 +96,10 @@ export const GettingStarted = () => {
)}
Getting started
-
{progressPercentage}%
+
{progressPercentage}%
-
+
{status === "completed" ? (
diff --git a/frontend/components/RichText.tsx b/frontend/components/RichText.tsx
index 883aa5087a..c95db2f8c3 100644
--- a/frontend/components/RichText.tsx
+++ b/frontend/components/RichText.tsx
@@ -30,15 +30,14 @@ const RichText = ({ content }: { content: Content }) => {
export const Editor = ({
value,
- invalid,
onChange,
className,
+ ...props
}: {
value: string | null;
- invalid?: boolean;
onChange: (value: string) => void;
className?: string;
-}) => {
+} & React.ComponentProps<"div">) => {
const [addingLink, setAddingLink] = useState<{ url: string } | null>(null);
const id = React.useId();
@@ -50,10 +49,7 @@ export const Editor = ({
editorProps: {
attributes: {
id,
- class: cn(className, "prose p-4 max-h-96 overflow-y-auto max-w-full rounded-b-md", {
- "outline-red": invalid,
- }),
- "aria-invalid": String(invalid),
+ class: cn(className, "prose p-4 min-h-60 max-h-96 overflow-y-auto max-w-full rounded-b-md outline-none"),
},
},
immediatelyRender: false,
@@ -93,8 +89,16 @@ export const Editor = ({
};
return (
-
-
+
+
{toolbarItems.map((item) => (
& {
+ containerClassName?: string;
+}) {
+ return (
+
+ );
+}
+
+function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return
;
+}
+
+function InputOTPSlot({
+ index,
+ className,
+ ...props
+}: React.ComponentProps<"div"> & {
+ index: number;
+}) {
+ const inputOTPContext = React.useContext(OTPInputContext);
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] ?? {};
+
+ return (
+
+ {char}
+ {hasFakeCaret ? (
+
+ ) : null}
+
+ );
+}
+
+function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
+ return (
+
+
+
+ );
+}
+
+export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot };
diff --git a/frontend/middleware.ts b/frontend/middleware.ts
index 2ca38c3a12..ae3f438e05 100644
--- a/frontend/middleware.ts
+++ b/frontend/middleware.ts
@@ -15,7 +15,7 @@ export default function middleware(req: NextRequest) {
const cspHeader = `
default-src 'self';
- script-src 'self' 'unsafe-inline' ${NODE_ENV === "production" ? "" : `'unsafe-eval'`};
+ script-src 'self' 'unsafe-inline' https://cdn.docuseal.com https://js.stripe.com ${NODE_ENV === "production" ? "" : `'unsafe-eval'`};
style-src 'self' 'unsafe-inline';
connect-src 'self' https://docuseal.com ${helperUrls} ${s3Urls};
img-src 'self' blob: data: https://docuseal.com https://docuseal.s3.amazonaws.com ${s3Urls};
diff --git a/package.json b/package.json
index 35d03298a1..d094c9c031 100644
--- a/package.json
+++ b/package.json
@@ -111,6 +111,7 @@
"drizzle-zod": "^0.7.0",
"immutable": "^5.0.3",
"inngest": "^3.27.0",
+ "input-otp": "^1.4.2",
"iso-3166": "^4.3.0",
"jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21",
diff --git a/playwright.config.ts b/playwright.config.ts
index babd9f54a0..9e32f612ce 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -33,7 +33,10 @@ export default defineConfig({
},
{
name: "chromium",
- use: { ...devices["Desktop Chrome"] },
+ use: {
+ ...devices["Desktop Chrome"],
+ viewport: { width: 1920, height: 1080 },
+ },
dependencies: ["setup"],
},
],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5780347e8f..42a804be9c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -199,6 +199,9 @@ importers:
inngest:
specifier: ^3.27.0
version: 3.32.7(patch_hash=b859654a83fe3c45179ca9dbe71b7b6917366a67657f37658b146b1264870e28)(next@15.2.4(patch_hash=b88b2148998305bbdabddd07fbda67d9c9f6010995b25120053815e04bd5330b)(@opentelemetry/api@1.9.0)(@playwright/test@1.51.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.8.2)
+ input-otp:
+ specifier: ^1.4.2
+ version: 1.4.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
iso-3166:
specifier: ^4.3.0
version: 4.3.0
@@ -4722,6 +4725,12 @@ packages:
typescript:
optional: true
+ input-otp@1.4.2:
+ resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==}
+ peerDependencies:
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+
internal-slot@1.1.0:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
@@ -11970,6 +11979,11 @@ snapshots:
- encoding
- supports-color
+ input-otp@1.4.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+
internal-slot@1.1.0:
dependencies:
es-errors: 1.3.0