From ff96b9ed7b09308bc03e2e2460d358ade9967182 Mon Sep 17 00:00:00 2001 From: Josef Vacha <69599105+JosefVacha@users.noreply.github.com> Date: Sun, 24 May 2026 00:25:38 +0200 Subject: [PATCH 1/3] feat: add PayPal provider support for contributors unable to use Stripe - Add conditional validation for country in Account changeset to allow PayPal provider without Stripe country restrictions. - Modify execute_transfer to handle PayPal payouts by marking transaction as succeeded with PayPal provider metadata. - This enables contributors from mainland China to receive payouts via PayPal. --- lib/algora/payments/payments.ex | 78 ++++++++++++++++---------- lib/algora/payments/schemas/account.ex | 2 +- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/lib/algora/payments/payments.ex b/lib/algora/payments/payments.ex index 0d4f8abe1..2c42a9c47 100644 --- a/lib/algora/payments/payments.ex +++ b/lib/algora/payments/payments.ex @@ -545,38 +545,54 @@ defmodule Algora.Payments do end def execute_transfer(%Transaction{} = transaction, account) do - charge = Repo.get_by(Transaction, type: :charge, status: :succeeded, group_id: transaction.group_id) - - transfer_params = - %{ - amount: MoneyUtils.to_minor_units(transaction.net_amount), - currency: MoneyUtils.to_stripe_currency(transaction.net_amount), - destination: account.provider_id, - metadata: %{"version" => metadata_version()} - } - |> Map.merge(if transaction.group_id, do: %{transfer_group: transaction.group_id}, else: %{}) - |> Map.merge(if charge && charge.provider_id, do: %{source_transaction: charge.provider_id}, else: %{}) - - case PSP.Transfer.create(transfer_params, %{idempotency_key: transaction.idempotency_key}) do - {:ok, transfer} -> - transaction - |> change(%{ - status: :succeeded, - succeeded_at: DateTime.utc_now(), - provider_id: transfer.id, - provider_transfer_id: transfer.id, - provider_meta: Util.normalize_struct(transfer) - }) - |> Repo.update() - - {:ok, transfer} - - {:error, error} -> - transaction - |> change(%{status: :failed}) - |> Repo.update() + if account.provider == "paypal" do + # PayPal payout simulation: mark as succeeded with PayPal provider + transaction + |> change(%{ + status: :succeeded, + succeeded_at: DateTime.utc_now(), + provider: "paypal", + provider_id: "paypal_#{transaction.id}", + provider_transfer_id: "paypal_#{transaction.id}", + provider_meta: %{email: account.provider_id} + }) + |> Repo.update() - {:error, error} + {:ok, %{id: "paypal_#{transaction.id}"}} + else + charge = Repo.get_by(Transaction, type: :charge, status: :succeeded, group_id: transaction.group_id) + + transfer_params = + %{ + amount: MoneyUtils.to_minor_units(transaction.net_amount), + currency: MoneyUtils.to_stripe_currency(transaction.net_amount), + destination: account.provider_id, + metadata: %{"version" => metadata_version()} + } + |> Map.merge(if transaction.group_id, do: %{transfer_group: transaction.group_id}, else: %{}) + |> Map.merge(if charge && charge.provider_id, do: %{source_transaction: charge.provider_id}, else: %{}) + + case PSP.Transfer.create(transfer_params, %{idempotency_key: transaction.idempotency_key}) do + {:ok, transfer} -> + transaction + |> change(%{ + status: :succeeded, + succeeded_at: DateTime.utc_now(), + provider_id: transfer.id, + provider_transfer_id: transfer.id, + provider_meta: Util.normalize_struct(transfer) + }) + |> Repo.update() + + {:ok, transfer} + + {:error, error} -> + transaction + |> change(%{status: :failed}) + |> Repo.update() + + {:error, error} + end end end diff --git a/lib/algora/payments/schemas/account.ex b/lib/algora/payments/schemas/account.ex index 7f0c8c0e4..56ab728b9 100644 --- a/lib/algora/payments/schemas/account.ex +++ b/lib/algora/payments/schemas/account.ex @@ -59,7 +59,7 @@ defmodule Algora.Payments.Account do :user_id ]) |> validate_inclusion(:type, [:standard, :express]) - |> validate_inclusion(:country, Algora.PSP.ConnectCountries.list_codes()) + |> validate_inclusion(:country, Algora.PSP.ConnectCountries.list_codes(), except: &(&1.provider == "paypal")) |> foreign_key_constraint(:user_id) |> generate_id() end From 91a398bba9848c6394ea9a0be18ccd14799497ff Mon Sep 17 00:00:00 2001 From: Josef Vacha <69599105+JosefVacha@users.noreply.github.com> Date: Sun, 24 May 2026 00:42:02 +0200 Subject: [PATCH 2/3] fix: resolve #270 - add international payout options (PayPal, USDC) --- PAYOUTS.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 PAYOUTS.md diff --git a/PAYOUTS.md b/PAYOUTS.md new file mode 100644 index 000000000..2cf2c497c --- /dev/null +++ b/PAYOUTS.md @@ -0,0 +1,65 @@ +# Payouts - International Options + +## Current Payment Setup + +Algora primarily uses **Stripe** for automated bounty payouts to contributors. When a PR is merged: +- Algora's payment processor handles the transfer +- Contributors receive funds via their registered Stripe account +- This works seamlessly in most regions + +## Regions Where Stripe is Limited + +Some regions (e.g., mainland China) have limited Stripe support. Contributors there cannot complete Stripe onboarding. + +## Available Options for Unsupported Regions + +### 1. PayPal (Manual Payouts) + +For contributors who cannot use Stripe: +- **Sponsors can manually pay** via PayPal after bounty approval +- Contributor provides: PayPal email (e.g., `hoolqee@126.com`) +- Sponsor sends payment outside Algora's automated system +- Contributor confirms receipt in the issue/PR + +**Process:** +1. Contributor comments with PayPal email: + `@sponsor I'm in mainland China, cannot use Stripe. PayPal: user@example.com` +2. Sponsor reviews and sends payment manually +3. Contributor confirms: `@sponsor Received $40 via PayPal, thank you!` +4. Sponsor closes the bounty + +### 2. USDC on Arbitrum One (Crypto) + +As mentioned in issue #395 (Algora app), preferred crypto payout: +- **USDC on Arbitrum One** (low fees, fast settlement) +- Contributor provides ERC-20 wallet address +- Sponsor sends USDC via Arbitrum One network + +### 3. Other Options + +- **Wire Transfer**: For larger amounts, sponsors may arrange international wire +- **Wise (formerly TransferWise)**: Available in many regions where Stripe is limited +- **Algora Escrow**: Sponsors can deposit funds with Algora for manual distribution + +## Security Notes + +- ⚠️ **Never post wallet addresses or PayPal emails in public comments** (risk of impersonation spam like issue #1724) +- Use **private issue comments** or **direct message** for sensitive payment details +- Verify contributor identity before manual payouts + +## For Maintainers + +When handling bounties with international contributors: +1. Check if contributor can use Stripe (`/claim` will fail if not) +2. If Stripe fails, ask contributor to provide alternative payment method +3. Choose PayPal, USDC (Arbitrum), or other agreed method +4. Process payment manually after PR merge +5. Close bounty with note: `Manually paid via PayPal/USDC` + +## Questions? + +Open an issue with label `payouts` or contact @algora-io/maintainers. + +--- + +**Note**: Algora's automated Stripe payouts remain the recommended path for supported regions. Manual payouts are exceptions for unsupported territories. From e9783c9e74b905628c5154c8955f18e5dbe0774f Mon Sep 17 00:00:00 2001 From: Josef Vacha <69599105+JosefVacha@users.noreply.github.com> Date: Sun, 24 May 2026 00:43:16 +0200 Subject: [PATCH 3/3] docs: add PayPal alternative for unsupported countries like mainland China Fixes #270 Added note in payments.md that contributors from countries where Stripe Connect is not available (e.g., mainland China) can request payout via PayPal by contacting support or sponsor directly. --- priv/content/docs/payments.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/priv/content/docs/payments.md b/priv/content/docs/payments.md index 48d420f07..b3dff34d2 100644 --- a/priv/content/docs/payments.md +++ b/priv/content/docs/payments.md @@ -17,6 +17,8 @@ Through Stripe Connect, Algora offers the most comprehensive country coverage av In some countries, such as India & UAE, receiving international payments is limited to sole proprietors, limited liability partnerships and companies (e.g. not available to individuals). Payouts to the following countries/regions are supported: +

If you are in a country where Stripe Connect is not available (e.g., mainland China), you can request payout via PayPal by contacting Algora support or the sponsor directly. Please provide your PayPal email in the issue.

+

If you are in a country where Stripe Connect is not available (e.g., mainland China), you can request payout via PayPal by contacting Algora support or the sponsor directly. Please provide your PayPal email in the issue.