Repository: thewca/worldcubeassociation.org
Pull request #11442: Refactor available refund route to general registration payment route
Author: login: FinnIckler
Created: 2025-04-29T09:52:37Z | Merged: 2025-04-30T00:20:11Z

<img width="1136" alt="image" src="https://github.com/user-attachments/assets/31bc932d-f430-46d6-b556-32779cd4d0c3" />
This is quite simple right now, but nested data in table is a design nightmare so I kept it like that for the sake of simplicity


Diff:
diff --git a/app/controllers/payment_controller.rb b/app/controllers/payment_controller.rb
index f235bcee70..b1f389718f 100644
--- a/app/controllers/payment_controller.rb
+++ b/app/controllers/payment_controller.rb
@@ -1,10 +1,10 @@
 # frozen_string_literal: true
 
 class PaymentController < ApplicationController
-  def available_refunds
+  def registration_payments
     if current_user
       registration_id = params.require(:registration_id)
-      registration = Registration.includes(:registration_payments).find(registration_id)
+      registration = Registration.includes(:competition, registration_payments: [:refunding_registration_payments]).find(registration_id)
 
       return render status: :bad_request, json: { error: "Registration not found" } if registration.blank?
 
@@ -30,6 +30,7 @@ def available_refunds
           human_amount_refundable: human_amount_refundable,
           human_amount_payment: human_amount_payment,
           currency_code: reg_payment.currency_code,
+          refunding_payments: reg_payment.refunding_registration_payments,
         }
       }
 
diff --git a/app/webpacker/components/RegistrationsV2/RegistrationEdit/Payments.jsx b/app/webpacker/components/RegistrationsV2/RegistrationEdit/Payments.jsx
new file mode 100644
index 0000000000..383fd9ae63
--- /dev/null
+++ b/app/webpacker/components/RegistrationsV2/RegistrationEdit/Payments.jsx
@@ -0,0 +1,139 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
+import React, { useEffect } from 'react';
+import {
+  Button, Message, Table,
+} from 'semantic-ui-react';
+import getRegistrationPayments from '../api/payment/get/getRegistrationPayments';
+import refundPayment from '../api/payment/get/refundPayment';
+import Loading from '../../Requests/Loading';
+import AutonumericField from '../../wca/FormBuilder/input/AutonumericField';
+import useInputState from '../../../lib/hooks/useInputState';
+import { useConfirm } from '../../../lib/providers/ConfirmProvider';
+import I18n from '../../../lib/i18n';
+import { isoMoneyToHumanReadable } from '../../../lib/helpers/money';
+
+export default function Payments({
+  onSuccess, registrationId, competitionId, competitorsInfo,
+}) {
+  const {
+    data: payments,
+    isLoading: paymentsLoading,
+    refetch,
+  } = useQuery({
+    queryKey: ['payments', registrationId],
+    queryFn: () => getRegistrationPayments(registrationId),
+    select: (data) => data.charges.filter((r) => r.ruby_amount_refundable !== 0),
+  });
+  const { mutate: refundMutation, isPending: isMutating } = useMutation({
+    mutationFn: refundPayment,
+    // The Backend will set a flash error on success or error
+    onSuccess: () => {
+      refetch();
+      onSuccess();
+    },
+  });
+
+  if (paymentsLoading) {
+    return <Loading />;
+  }
+
+  if (payments.length === 0) {
+    return <Message warning>{I18n.t('payments.messages.charges_refunded')}</Message>;
+  }
+
+  return (
+    <Table>
+      <Table.Header>
+        <Table.Row>
+          <Table.HeaderCell>{I18n.t('payments.labels.net_payment')}</Table.HeaderCell>
+          <Table.HeaderCell>{I18n.t('payments.labels.original_payment')}</Table.HeaderCell>
+          <Table.HeaderCell>{I18n.t('registrations.refund_form.labels.refund_amount')}</Table.HeaderCell>
+          <Table.HeaderCell />
+        </Table.Row>
+      </Table.Header>
+      <Table.Body>
+        {payments.map((refund) => (
+          <PaymentRow
+            payment={refund}
+            refundMutation={refundMutation}
+            isMutating={isMutating}
+            competitionId={competitionId}
+            key={refund.payment_id}
+            competitorsInfo={competitorsInfo}
+          />
+        ))}
+      </Table.Body>
+    </Table>
+  );
+}
+
+function PaymentRow({
+  payment, refundMutation, isMutating, competitionId, competitorsInfo,
+}) {
+  const [amountToRefund, setAmountToRefund] = useInputState(payment.ruby_amount_refundable);
+
+  // React state persists across rerenders, so `amountToRefund` would keep old values
+  // which is problematic when refunding more than 50% of the original
+  // (because then the input exceeds the new max, leading to a whole new tragedy with AN)
+  useEffect(() => {
+    setAmountToRefund((prevAmount) => Math.min(prevAmount, payment.ruby_amount_refundable));
+  }, [payment.ruby_amount_refundable, setAmountToRefund]);
+
+  const confirm = useConfirm();
+
+  const attemptRefund = () => confirm({
+    content: I18n.t('registrations.refund_confirmation'),
+  }).then(() => {
+    refundMutation({
+      competitionId,
+      paymentId: payment.payment_id,
+      paymentProvider: payment.payment_provider,
+      amount: amountToRefund,
+    });
+  });
+
+  return (
+    <>
+      <Table.Row>
+        <Table.Cell>
+          {payment.human_amount_refundable}
+        </Table.Cell>
+        <Table.Cell>
+          {payment.human_amount_payment}
+        </Table.Cell>
+        <Table.Cell>
+          <AutonumericField
+            currency={payment.currency_code.toUpperCase()}
+            value={amountToRefund}
+            onChange={setAmountToRefund}
+            max={payment.ruby_amount_refundable}
+          />
+        </Table.Cell>
+        <Table.Cell>
+          <Button
+            onClick={attemptRefund}
+            disabled={isMutating}
+          >
+            {I18n.t('registrations.refund')}
+          </Button>
+        </Table.Cell>
+      </Table.Row>
+      {payment.refunding_payments.map((p) => (
+        <Table.Row>
+          <Table.Cell />
+          <Table.Cell />
+          <Table.Cell>
+            {isoMoneyToHumanReadable(Math.abs(p.amount_lowest_denomination), p.currency_code)}
+          </Table.Cell>
+          <Table.Cell>
+            Refunded by
+            {' '}
+            {competitorsInfo.find(
+              (c) => c.id === Number(p.user_id),
+            )?.name}
+          </Table.Cell>
+        </Table.Row>
+      ))}
+    </>
+  );
+}
diff --git a/app/webpacker/components/RegistrationsV2/RegistrationEdit/Refunds.jsx b/app/webpacker/components/RegistrationsV2/RegistrationEdit/Refunds.jsx
deleted file mode 100644
index 43268f23fb..0000000000
--- a/app/webpacker/components/RegistrationsV2/RegistrationEdit/Refunds.jsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import { useMutation, useQuery } from '@tanstack/react-query';
-import React, { useEffect } from 'react';
-import {
-  Button, Header, Message, Table,
-} from 'semantic-ui-react';
-import getAvailableRefunds from '../api/payment/get/getAvailableRefunds';
-import refundPayment from '../api/payment/get/refundPayment';
-import Loading from '../../Requests/Loading';
-import AutonumericField from '../../wca/FormBuilder/input/AutonumericField';
-import useInputState from '../../../lib/hooks/useInputState';
-import { useConfirm } from '../../../lib/providers/ConfirmProvider';
-import I18n from '../../../lib/i18n';
-
-export default function Refunds({
-  onSuccess, registrationId, competitionId,
-}) {
-  const {
-    data: refunds,
-    isLoading: refundsLoading,
-    refetch,
-  } = useQuery({
-    queryKey: ['refunds', registrationId],
-    queryFn: () => getAvailableRefunds(registrationId),
-    refetchOnWindowFocus: false,
-    refetchOnReconnect: false,
-    staleTime: Infinity,
-    refetchOnMount: 'always',
-    select: (data) => data.charges.filter((r) => r.ruby_amount_refundable !== 0),
-  });
-  const { mutate: refundMutation, isPending: isMutating } = useMutation({
-    mutationFn: refundPayment,
-    // The Backend will set a flash error on success or error
-    onSuccess: () => {
-      refetch();
-      onSuccess();
-    },
-  });
-
-  if (refundsLoading) {
-    return <Loading />;
-  }
-
-  if (refunds.length === 0) {
-    return <Message warning>{I18n.t('payments.messages.charges_refunded')}</Message>;
-  }
-
-  return (
-    <>
-      <Header>{I18n.t('payments.labels.available_refunds')}</Header>
-      <Table>
-        <Table.Header>
-          <Table.Row>
-            <Table.HeaderCell>{I18n.t('payments.labels.original_payment')}</Table.HeaderCell>
-            <Table.HeaderCell>{I18n.t('registrations.refund_form.hints.refund_amount')}</Table.HeaderCell>
-            <Table.HeaderCell>{I18n.t('registrations.refund_form.labels.refund_amount')}</Table.HeaderCell>
-            <Table.HeaderCell />
-          </Table.Row>
-        </Table.Header>
-        <Table.Body>
-          {refunds.map((refund) => (
-            <RefundRow
-              refund={refund}
-              refundMutation={refundMutation}
-              isMutating={isMutating}
-              competitionId={competitionId}
-              key={refund.payment_id}
-            />
-          ))}
-        </Table.Body>
-      </Table>
-    </>
-  );
-}
-
-function RefundRow({
-  refund, refundMutation, isMutating, competitionId,
-}) {
-  const [amountToRefund, setAmountToRefund] = useInputState(refund.ruby_amount_refundable);
-
-  // React state persists across rerenders, so `amountToRefund` would keep old values
-  // which is problematic when refunding more than 50% of the original
-  // (because then the input exceeds the new max, leading to a whole new tragedy with AN)
-  useEffect(() => {
-    setAmountToRefund((prevAmount) => Math.min(prevAmount, refund.ruby_amount_refundable));
-  }, [refund.ruby_amount_refundable, setAmountToRefund]);
-
-  const confirm = useConfirm();
-
-  const attemptRefund = () => confirm({
-    content: I18n.t('registrations.refund_confirmation'),
-  }).then(() => {
-    refundMutation({
-      competitionId,
-      paymentId: refund.payment_id,
-      paymentProvider: refund.payment_provider,
-      amount: amountToRefund,
-    });
-  });
-
-  return (
-    <Table.Row>
-      <Table.Cell>
-        {refund.human_amount_payment}
-      </Table.Cell>
-      <Table.Cell>
-        {refund.human_amount_refundable}
-      </Table.Cell>
-      <Table.Cell>
-        <AutonumericField
-          currency={refund.currency_code.toUpperCase()}
-          value={amountToRefund}
-          onChange={setAmountToRefund}
-          max={refund.ruby_amount_refundable}
-        />
-      </Table.Cell>
-      <Table.Cell>
-        <Button
-          onClick={attemptRefund}
-          disabled={isMutating}
-        >
-          {I18n.t('registrations.refund')}
-        </Button>
-      </Table.Cell>
-    </Table.Row>
-  );
-}
diff --git a/app/webpacker/components/RegistrationsV2/RegistrationEdit/RegistrationEditor.jsx b/app/webpacker/components/RegistrationsV2/RegistrationEdit/RegistrationEditor.jsx
index 811dfbdfb4..c1210da2df 100644
--- a/app/webpacker/components/RegistrationsV2/RegistrationEdit/RegistrationEditor.jsx
+++ b/app/webpacker/components/RegistrationsV2/RegistrationEdit/RegistrationEditor.jsx
@@ -10,7 +10,6 @@ import {
   Form,
   Header,
   Icon,
-  List,
   Message,
   Segment,
 } from 'semantic-ui-react';
@@ -19,7 +18,7 @@ import { useDispatch } from '../../../lib/providers/StoreProvider';
 import { showMessage } from '../Register/RegistrationMessage';
 import Loading from '../../Requests/Loading';
 import EventSelector from '../../wca/EventSelector';
-import Refunds from './Refunds';
+import Payments from './Payments';
 import { personUrl, editPersonUrl } from '../../../lib/requests/routes.js.erb';
 import { useConfirm } from '../../../lib/providers/ConfirmProvider';
 import I18n from '../../../lib/i18n';
@@ -299,21 +298,13 @@ export default function RegistrationEditor({ registrationId, competitor, competi
 
       {competitionInfo['using_payment_integrations?'] && (
         <>
-          <Message>{I18n.t('payments.labels.payment_statuses')}</Message>
-          <List>
-            {registration.payment.payment_statuses.map((paymentStatus) => (
-              <List.Item key={paymentStatus}>
-                {/* i18n-tasks-use t('payments.statuses.succeeded') */}
-                {/* i18n-tasks-use t('payments.statuses.refund') */}
-                {I18n.t(`payments.statuses.${paymentStatus}`)}
-              </List.Item>
-            ))}
-          </List>
+          <Header>Payments</Header>
           {(registration.payment.payment_statuses.includes('succeeded') || registration.payment.payment_statuses.includes('refund')) && (
-            <Refunds
+            <Payments
               competitionId={competitionInfo.id}
               registrationId={registrationId}
               onSuccess={refetch}
+              competitorsInfo={competitorsInfo}
             />
           )}
         </>
diff --git a/app/webpacker/components/RegistrationsV2/api/payment/get/getAvailableRefunds.js b/app/webpacker/components/RegistrationsV2/api/payment/get/getAvailableRefunds.js
deleted file mode 100644
index afa0352231..0000000000
--- a/app/webpacker/components/RegistrationsV2/api/payment/get/getAvailableRefunds.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { fetchJsonOrError } from '../../../../../lib/requests/fetchWithAuthenticityToken';
-import { availableRefundsUrl } from '../../../../../lib/requests/routes.js.erb';
-
-export default async function getAvailableRefunds(
-  registrationId,
-) {
-  const { data } = await fetchJsonOrError(availableRefundsUrl(registrationId));
-  return data;
-}
diff --git a/app/webpacker/components/RegistrationsV2/api/payment/get/getRegistrationPayments.js b/app/webpacker/components/RegistrationsV2/api/payment/get/getRegistrationPayments.js
new file mode 100644
index 0000000000..0fb6fed98f
--- /dev/null
+++ b/app/webpacker/components/RegistrationsV2/api/payment/get/getRegistrationPayments.js
@@ -0,0 +1,9 @@
+import { fetchJsonOrError } from '../../../../../lib/requests/fetchWithAuthenticityToken';
+import { registrationPaymentsUrl } from '../../../../../lib/requests/routes.js.erb';
+
+export default async function getRegistrationPayments(
+  registrationId,
+) {
+  const { data } = await fetchJsonOrError(registrationPaymentsUrl(registrationId));
+  return data;
+}
diff --git a/app/webpacker/lib/requests/routes.js.erb b/app/webpacker/lib/requests/routes.js.erb
index 7819262ff8..0be66abe28 100644
--- a/app/webpacker/lib/requests/routes.js.erb
+++ b/app/webpacker/lib/requests/routes.js.erb
@@ -97,7 +97,7 @@ export const adminFixResultsUrl = (personId, competition_id = undefined, event_i
 
 export const paymentFinishUrl = (competitionId, paymentIntegration) => `<%= CGI.unescape(Rails.application.routes.url_helpers.registration_payment_completion_url("${competitionId}", "${paymentIntegration}", host: EnvConfig.ROOT_URL)) %>`
 
-export const availableRefundsUrl = (registrationId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.available_refunds_path("${registrationId}")) %>`
+export const registrationPaymentsUrl = (registrationId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.registration_payments_path("${registrationId}")) %>`
 
 export const refundPaymentUrl = (competitionId, paymentIntegration, paymentId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.registration_payment_refund_path("${competitionId}", "${paymentIntegration}", "${paymentId}")) %>`
 
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9b0dd38dec..e76ef5daef 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1260,8 +1260,6 @@ en:
     will_pay_here: "You will pay them here after submitting your registration"
     wont_pay_here: "Please check the competition's information for instructions about how to pay"
     refund_form:
-      hints:
-        refund_amount: "The amount to refund to the competitor. Can be any value up to the amount already paid."
       labels:
         refund_amount: "Refund Amount"
     #context: on the payment form, when registering to a competition
@@ -2235,8 +2233,7 @@ en:
     messages:
       charges_refunded: "All charges have been refunded"
     labels:
-      payment_statuses: "Payment Statuses"
-      available_refunds: "Available Refunds"
+      net_payment: "Net Payment"
       original_payment: "Original Payment"
     #context: localized names of payment providers
     payment_providers:
@@ -2274,9 +2271,6 @@ en:
           display_name: "Name"
           primary_email: "Email"
         fallback_not_applicable: "N/A"
-    statuses:
-      succeeded: "Succeeded"
-      refund: "Refunded"
   persons:
     #context: Person search page
     index:
diff --git a/config/routes.rb b/config/routes.rb
index 77644ea2ac..627e9d2289 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -97,7 +97,9 @@
     post 'registrations/add' => 'registrations#do_add', as: :registrations_do_add
     get 'registrations/psych-sheet' => 'registrations#psych_sheet', as: :psych_sheet
     get 'registrations/psych-sheet/:event_id' => 'registrations#psych_sheet_event', as: :psych_sheet_event
-    resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true
+    resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true do
+      get 'payments' => 'payment#registration_payments', as: :payments
+    end
     get 'edit/registrations' => 'registrations#edit_registrations'
     get 'register' => 'registrations#register'
     resources :competition_tabs, except: [:show], as: :tabs, path: :tabs
@@ -123,9 +125,6 @@
     get '/payment_integration/:payment_integration/connect' => 'competitions#connect_payment_integration', as: :connect_payment_integration
     post '/payment_integration/:payment_integration/disconnect' => 'competitions#disconnect_payment_integration', as: :disconnect_payment_integration
   end
-  scope :payment do
-    get '/refunds/:registration_id' => 'payment#available_refunds', as: :available_refunds
-  end
 
   get 'competitions/:competition_id/report/edit' => 'delegate_reports#edit', as: :delegate_report_edit
   get 'competitions/:competition_id/report' => 'delegate_reports#show', as: :delegate_report


Reviews:
user: login: gregorbg
body: I think we should merge https://github.com/thewca/worldcubeassociation.org/pull/11440 first (which Duncan probably?? approved by accident, we were talking about another PR privately on Slack, so please don't rush ahead with merging that one preemptively...) and that will lead to a trivial number of merge conflicts (for example, including the `id` in the registration V2 JSON).

Also, regarding the table layout: Have you tried adding a second row if there are refunding payments, and give that a `colSpan` to cover the whole row? You could easily add a list in that one (or even consider giving it more columns to show for example who the refund was issued by).
I won't absolutely insist on this feature, but it would be nice to have a bit more details on the refunds. See if you can find something that works, and if it's crazy hard or always looks :hankey: then report back here and we will merge as-is and think about refund details later :tm: 
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-29T10:06:49Z
user: login: FinnIckler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T10:34:39Z
user: login: gregorbg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T11:09:25Z
user: login: gregorbg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T11:30:08Z
user: login: FinnIckler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T11:32:35Z
user: login: gregorbg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T11:32:53Z
user: login: FinnIckler
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T11:33:49Z
user: login: gregorbg
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-29T12:03:27Z
user: login: gregorbg
body: Cool, fine by me now! Do you have any strong preference whether we should merge this one or https://github.com/thewca/worldcubeassociation.org/pull/11440 first?
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T12:07:26Z

Review comments:
diff_hunk: @@ -99,6 +99,7 @@
     get 'registrations/psych-sheet' => 'registrations#psych_sheet', as: :psych_sheet
     get 'registrations/psych-sheet/:event_id' => 'registrations#psych_sheet_event', as: :psych_sheet_event
     resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true
+    get 'registrations/:registration_id/payment' => 'payment#registration_payments', as: :registration_payments
path: config/routes.rb
user: login: gregorbg
body: Nit 1: `/payments` with plural S in the URL string
created_at: 2025-04-29T10:00:08Z
author_association: MEMBER
diff_hunk: @@ -99,6 +99,7 @@
     get 'registrations/psych-sheet' => 'registrations#psych_sheet', as: :psych_sheet
     get 'registrations/psych-sheet/:event_id' => 'registrations#psych_sheet_event', as: :psych_sheet_event
     resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true
+    get 'registrations/:registration_id/payment' => 'payment#registration_payments', as: :registration_payments
path: config/routes.rb
user: login: gregorbg
body: Nit 2: You can probably move this under the `resources :registrations` block with a `do`:
```suggestion
    resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true do
      get 'payments' => 'payment#registration_payments', as: :registration_payments
    end
```
created_at: 2025-04-29T10:01:03Z
author_association: MEMBER
diff_hunk: @@ -99,6 +99,7 @@
     get 'registrations/psych-sheet' => 'registrations#psych_sheet', as: :psych_sheet
     get 'registrations/psych-sheet/:event_id' => 'registrations#psych_sheet_event', as: :psych_sheet_event
     resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true
+    get 'registrations/:registration_id/payment' => 'payment#registration_payments', as: :registration_payments
path: config/routes.rb
user: login: gregorbg
body: Although from the top of my head I wouldn't know how this affects the naming of the route helper. You will have to try that out locally.
created_at: 2025-04-29T10:01:32Z
author_association: MEMBER
diff_hunk: @@ -1,26 +1,27 @@
 import { useMutation, useQuery } from '@tanstack/react-query';
 import React, { useEffect } from 'react';
 import {
-  Button, Header, Message, Table,
+  Button, List, Message, Table,
 } from 'semantic-ui-react';
-import getAvailableRefunds from '../api/payment/get/getAvailableRefunds';
+import getRegistrationPayments from '../api/payment/get/getRegistrationPayments';
 import refundPayment from '../api/payment/get/refundPayment';
 import Loading from '../../Requests/Loading';
 import AutonumericField from '../../wca/FormBuilder/input/AutonumericField';
 import useInputState from '../../../lib/hooks/useInputState';
 import { useConfirm } from '../../../lib/providers/ConfirmProvider';
 import I18n from '../../../lib/i18n';
+import { isoMoneyToHumanReadable } from '../../../lib/helpers/money';
 
-export default function Refunds({
-  onSuccess, userId, competitionId,
+export default function Payments({
+  onSuccess, registrationId, competitionId,
 }) {
   const {
-    data: refunds,
-    isLoading: refundsLoading,
+    data: payments,
+    isLoading: paymentsLoading,
     refetch,
   } = useQuery({
-    queryKey: ['refunds', competitionId, userId],
-    queryFn: () => getAvailableRefunds(competitionId, userId),
+    queryKey: ['payments', registrationId],
+    queryFn: () => getRegistrationPayments(competitionId, registrationId),
     refetchOnWindowFocus: false,
     refetchOnReconnect: false,
     staleTime: Infinity,
path: app/webpacker/components/RegistrationsV2/RegistrationEdit/Payments.jsx
user: login: gregorbg
body: Technically unrelated but very, very easy fix: Aren't these options (and the fourth one below, which GitHub doesn't let me comment on because it arbitrarily rules that it's too far away from the changed lines, grr....) part of the default `WCAQueryClientProvider` setup these days?
created_at: 2025-04-29T10:03:34Z
author_association: MEMBER
diff_hunk: @@ -99,6 +99,7 @@
     get 'registrations/psych-sheet' => 'registrations#psych_sheet', as: :psych_sheet
     get 'registrations/psych-sheet/:event_id' => 'registrations#psych_sheet_event', as: :psych_sheet_event
     resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true
+    get 'registrations/:registration_id/payment' => 'payment#registration_payments', as: :registration_payments
path: config/routes.rb
user: login: FinnIckler
body: well, then we should move all those routes there right?
created_at: 2025-04-29T10:34:39Z
author_association: MEMBER
diff_hunk: @@ -99,6 +99,7 @@
     get 'registrations/psych-sheet' => 'registrations#psych_sheet', as: :psych_sheet
     get 'registrations/psych-sheet/:event_id' => 'registrations#psych_sheet_event', as: :psych_sheet_event
     resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true
+    get 'registrations/:registration_id/payment' => 'payment#registration_payments', as: :registration_payments
path: config/routes.rb
user: login: gregorbg
body: In principle: Yes. But that would be too much out of scope of this PR. (Sidenote: A `routes.rb` cleanup is long overdue anyways...)
created_at: 2025-04-29T11:09:25Z
author_association: MEMBER
diff_hunk: @@ -97,7 +97,7 @@ export const adminFixResultsUrl = (personId, competition_id = undefined, event_i
 
 export const paymentFinishUrl = (competitionId, paymentIntegration) => `<%= CGI.unescape(Rails.application.routes.url_helpers.registration_payment_completion_url("${competitionId}", "${paymentIntegration}", host: EnvConfig.ROOT_URL)) %>`
 
-export const availableRefundsUrl = (competitionId, userId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.refunds_path) %>?attendee_id=${competitionId}-${userId}`
+export const registrationPaymentsUrl = (competitionId, registrationId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.competition_registration_payments_path("${competitionId}", "${registrationId}")) %>`
path: app/webpacker/lib/requests/routes.js.erb
user: login: gregorbg
body: If I'm reading the `routes.rb` correctly, then `competitionId` is not even needed as a route parameter anymore
created_at: 2025-04-29T11:29:27Z
author_association: MEMBER
diff_hunk: @@ -97,7 +97,7 @@ export const adminFixResultsUrl = (personId, competition_id = undefined, event_i
 
 export const paymentFinishUrl = (competitionId, paymentIntegration) => `<%= CGI.unescape(Rails.application.routes.url_helpers.registration_payment_completion_url("${competitionId}", "${paymentIntegration}", host: EnvConfig.ROOT_URL)) %>`
 
-export const availableRefundsUrl = (competitionId, userId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.refunds_path) %>?attendee_id=${competitionId}-${userId}`
+export const registrationPaymentsUrl = (competitionId, registrationId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.competition_registration_payments_path("${competitionId}", "${registrationId}")) %>`
path: app/webpacker/lib/requests/routes.js.erb
user: login: FinnIckler
body: it is, it's under the   `resources :competitions do`
created_at: 2025-04-29T11:32:35Z
author_association: MEMBER
diff_hunk: @@ -97,7 +97,7 @@ export const adminFixResultsUrl = (personId, competition_id = undefined, event_i
 
 export const paymentFinishUrl = (competitionId, paymentIntegration) => `<%= CGI.unescape(Rails.application.routes.url_helpers.registration_payment_completion_url("${competitionId}", "${paymentIntegration}", host: EnvConfig.ROOT_URL)) %>`
 
-export const availableRefundsUrl = (competitionId, userId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.refunds_path) %>?attendee_id=${competitionId}-${userId}`
+export const registrationPaymentsUrl = (competitionId, registrationId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.competition_registration_payments_path("${competitionId}", "${registrationId}")) %>`
path: app/webpacker/lib/requests/routes.js.erb
user: login: gregorbg
body: Oh wait, that trick only worked for `resources :registrations` because it's marked as `shallow: true`. In that case, I think you should really consider my other comment above about nesting the route, because otherwise supplying `competitionId` *and* `registrationId` (which implicitly also identifies the competition) is kinda redundant. Nesting would make this route become simpler and profit from the `shallow` routing as well (at least I hope so)
created_at: 2025-04-29T11:32:53Z
author_association: MEMBER
diff_hunk: @@ -97,7 +97,7 @@ export const adminFixResultsUrl = (personId, competition_id = undefined, event_i
 
 export const paymentFinishUrl = (competitionId, paymentIntegration) => `<%= CGI.unescape(Rails.application.routes.url_helpers.registration_payment_completion_url("${competitionId}", "${paymentIntegration}", host: EnvConfig.ROOT_URL)) %>`
 
-export const availableRefundsUrl = (competitionId, userId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.refunds_path) %>?attendee_id=${competitionId}-${userId}`
+export const registrationPaymentsUrl = (competitionId, registrationId) => `<%= CGI.unescape(Rails.application.routes.url_helpers.competition_registration_payments_path("${competitionId}", "${registrationId}")) %>`
path: app/webpacker/lib/requests/routes.js.erb
user: login: FinnIckler
body: ok, I'll nest it
created_at: 2025-04-29T11:33:49Z
author_association: MEMBER
diff_hunk: @@ -98,7 +98,9 @@
     post 'registrations/add' => 'registrations#do_add', as: :registrations_do_add
     get 'registrations/psych-sheet' => 'registrations#psych_sheet', as: :psych_sheet
     get 'registrations/psych-sheet/:event_id' => 'registrations#psych_sheet_event', as: :psych_sheet_event
-    resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true
+    resources :registrations, only: [:index, :update, :create, :edit, :destroy], shallow: true do
+      get ':registration_id/payments' => 'payment#registration_payments', as: :payments
path: config/routes.rb
user: login: gregorbg
body: Sorry for picking so much on your `routes.rb`, but the fact that it's nested under `resources :registrations` adds a `:registration_id` route param automagically. `get 'payments'` is literally enough to achieve what you want.
created_at: 2025-04-29T12:03:22Z
author_association: MEMBER

PR comments:
user: login: FinnIckler
created_at: 2025-04-29T10:58:26Z
body: <img width="1175" alt="image" src="https://github.com/user-attachments/assets/ad566785-2369-41fb-8701-69c7b1a85f15" />
what about this
author_association: MEMBER
user: login: gregorbg
created_at: 2025-04-29T11:10:12Z
body: > what about this

Awesome, I like it much more than the previous version :heart_eyes: 

author_association: MEMBER
user: login: FinnIckler
created_at: 2025-04-29T12:11:37Z
body: I don't have any preference, but I can review the other PR in like an hour
author_association: MEMBER