Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 6 additions & 24 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sensitive secret is exposed to every step by defining it in the top-level env block; limit it to only the job or step that actually needs it.

Prompt for AI agents
Address the following comment on .github/workflows/tests.yml at line 8:

<comment>Sensitive secret is exposed to every step by defining it in the top-level env block; limit it to only the job or step that actually needs it.</comment>

<file context>
@@ -2,12 +2,11 @@ name: Tests
 
 on:
   push:
-
-concurrency:
-  group: ${{ github.workflow }}-${{ github.ref }}
-  cancel-in-progress: ${{ github.ref != &#39;refs/heads/main&#39; }}
+  workflow_dispatch:
 
</file context>

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 }}
Expand All @@ -16,7 +15,7 @@ env:

jobs:
rspec:
runs-on: ubicloud-standard-2
runs-on: ubuntu-24.04

services:
postgres:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion backend/app/controllers/concerns/set_current.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of AcceptCompanyInviteLink#perform (which signals success or failure) is discarded, so any validation or persistence errors are silently ignored and the request continues as if the invite were accepted.

Prompt for AI agents
Address the following comment on backend/app/controllers/concerns/set_current.rb at line 29:

<comment>The result of AcceptCompanyInviteLink#perform (which signals success or failure) is discarded, so any validation or persistence errors are silently ignored and the request continues as if the invite were accepted.</comment>

<file context>
@@ -26,7 +26,7 @@ def set_current
     if user &amp;&amp; cookies[&quot;invitation_token&quot;].present?
       invite_link = CompanyInviteLink.find_by(token: cookies[&quot;invitation_token&quot;])
       invited_company = invite_link&amp;.company
-      user.update!(signup_invite_link: invite_link) if invite_link
+      AcceptCompanyInviteLink.new(user:, token: invite_link.token).perform if invite_link
       cookies.delete(&quot;invitation_token&quot;)
     end
</file context>

cookies.delete("invitation_token")
end

Expand Down
11 changes: 11 additions & 0 deletions backend/app/mailers/company_worker_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Email subject exposes the raw database id; use the public-facing invoice_number instead

Prompt for AI agents
Address the following comment on backend/app/mailers/company_worker_mailer.rb at line 88:

<comment>Email subject exposes the raw database id; use the public-facing invoice_number instead</comment>

<file context>
@@ -77,6 +77,17 @@ def payment_failed_reenter_bank_details(payment_id, amount, currency)
     mail(to: user.email, reply_to: company.email, subject: &quot;🔴 Payment failed: re-enter your bank details&quot;)
   end
 
+  def payment_failed(payment_id, amount, currency)
+    @payment = Payment.find(payment_id)
+    @invoice = @payment.invoice
+    @currency = currency
+    @amount = amount
+    company = @invoice.company
</file context>
Suggested change
mail(to: user.email, reply_to: company.email, subject: "🔴 Payment failed: payment failure for invoice ##{@invoice.id}")
mail(to: user.email, reply_to: company.email, subject: "🔴 Payment failed for invoice #{@invoice.invoice_number}")

end

def equity_percent_selection(company_worker_id)
company_worker = CompanyWorker.find(company_worker_id)
@company = company_worker.company
Expand Down
1 change: 0 additions & 1 deletion backend/app/services/pay_investor_dividends.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
1 change: 0 additions & 1 deletion backend/app/services/pay_investor_equity_buybacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
11 changes: 10 additions & 1 deletion backend/app/services/pay_invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Email shows a USD amount even when the currency argument is non-USD, because amount is not converted to the target currency.

Prompt for AI agents
Address the following comment on backend/app/services/pay_invoice.rb at line 72:

<comment>Email shows a USD amount even when the currency argument is non-USD, because `amount` is not converted to the target currency.</comment>

<file context>
@@ -68,6 +67,16 @@ def process
   rescue WiseError =&gt; e
     payment.update!(status: Payment::FAILED)
     invoice.update!(status: Invoice::FAILED)
+
+    target_currency = payment.wise_recipient&amp;.currency || &quot;USD&quot;
+    amount = payment.cash_amount_in_usd
+
+    if e.message.include?(&quot;Bank account is no longer active&quot;)
</file context>


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
5 changes: 5 additions & 0 deletions backend/app/sidekiq/wise_transfer_update_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second consecutive call to api_service.get_transfer duplicates a network request that was already performed above, causing unnecessary external API traffic and latency.

Prompt for AI agents
Address the following comment on backend/app/sidekiq/wise_transfer_update_job.rb at line 40:

<comment>Second consecutive call to api_service.get_transfer duplicates a network request that was already performed above, causing unnecessary external API traffic and latency.</comment>

<file context>
@@ -36,6 +36,11 @@ def perform(params)
         if payment.is_a?(Payment)
           amount_cents = api_service.get_transfer(transfer_id:)[&quot;sourceValue&quot;] * -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[&quot;targetValue&quot;]
+          currency = transfer_details[&quot;targetCurrency&quot;]
</file context>

amount = transfer_details["targetValue"]
currency = transfer_details["targetCurrency"]
CompanyWorkerMailer.payment_failed(payment.id, amount, currency).deliver_later
end
end
invoice.update!(status: Invoice::FAILED)
Expand Down
34 changes: 34 additions & 0 deletions backend/app/views/company_worker_mailer/payment_failed.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<h1>
There was an issue processing your payment of <%= Money.from_amount(@amount, @currency).format(with_currency: true, symbol: false, no_cents_if_whole: true) %>
</h1>
<dl>
<dt>Invoice ID</dt>
<dd>
<%= @invoice.invoice_number %>
</dd>
<dt>Invoice amount</dt>
<dd>
<%= cents_format(@invoice.total_amount_in_usd_cents, with_currency: true, symbol: false) %>
</dd>
<dt>Bank account</dt>
<dd>
****<%= @payment.recipient_last4 %>
</dd>
</dl>
<p>
Unfortunately, there was an issue processing your payment through Wise. This could be due to:
</p>
<ul>
<li>Incorrect bank account information</li>
<li>Bank transfer restrictions</li>
<li>Compliance or verification issues</li>
<li>Processing errors at the banking institution</li>
</ul>

<p>
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.
</p>

<p>
We'll attempt to process your payment again once the issue is resolved.
</p>
6 changes: 1 addition & 5 deletions backend/app/views/user_mailer/otp_code.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
<h1>Your verification code</h1>

<p>Your verification code is: <strong><%= @otp_code %></strong></p>
<h1>Your verification code is <%= @otp_code %></h1>

<p>This code will expire in 10 minutes. If you didn't request this code, please ignore this email.</p>

<p>If you're having trouble, contact us at <a href="mailto:<%= ApplicationMailer::SUPPORT_EMAIL %>"><%= ApplicationMailer::SUPPORT_EMAIL %></a>.</p>
Loading
Loading