Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Euros #583

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c304f92
Add Euros to currency options
duncte123 Jul 26, 2023
48bd336
Support euros in the front-end
duncte123 Jul 26, 2023
1f367ba
Store the currency in the store
duncte123 Jul 27, 2023
83bc8c3
Set default currency in reducer to make the tests happy
duncte123 Jul 27, 2023
8c43244
Clarify comment
duncte123 Jul 27, 2023
f3fded7
Display donation currency on reading and processing pages
duncte123 Jul 28, 2023
25a86d6
Extract currency from donation for processing bundle
duncte123 Jul 30, 2023
52e0bda
Fix the prize grid and prize page not showing the proper currency
duncte123 Aug 14, 2023
a418860
Overlooked tests
duncte123 Aug 14, 2023
b6aeb61
Merge branch 'master' into feat/euros
duncte123 Aug 14, 2023
a530978
Merge migrations
duncte123 Aug 14, 2023
e731d71
Merge branch 'master' into feat/euros
duncte123 Aug 24, 2023
6be0c20
Merge branch 'master' into feat/euros
duncte123 Sep 9, 2023
6180d2b
Merge branch 'master' into feat/euros
duncte123 Nov 15, 2023
cb749ce
Merge branch 'master' into feat/euros
duncte123 Nov 22, 2023
d7a4ab2
Merge master
duncte123 Dec 31, 2023
940ef6c
Merge branch 'master' into feat/euros
duncte123 Jan 1, 2024
68ae6a7
Merge branch 'master' into feat/euros
duncte123 Jan 23, 2024
d893643
add `start` field to Milestone model (#647)
uraniumanchor Feb 13, 2024
60de322
add video links (#648)
uraniumanchor Feb 14, 2024
51473b8
always sort donations on read page (#649)
uraniumanchor Feb 14, 2024
ce9b690
[apiv2] Add more fields to runs to match v1 details (#651)
faultyserver Feb 17, 2024
9e2bda0
fix Milestone start validation (#654)
uraniumanchor Feb 21, 2024
44c46fa
Bump selenium from 4.16.0 to 4.18.1 (#653)
dependabot[bot] Feb 21, 2024
227aadf
Update responses requirement from ~=0.24.1 to ~=0.25.0 (#650)
dependabot[bot] Feb 21, 2024
825a81e
Bump pre-commit from 3.5.0 to 3.6.2 (#652)
dependabot[bot] Feb 21, 2024
badf7c9
Merge branch 'master' into feat/euros
duncte123 Mar 11, 2024
b89ba9f
Lint
duncte123 Mar 12, 2024
decf09e
Merge remote-tracking branch 'upstream/master' into feat/euros
duncte123 Apr 13, 2024
71ee57f
Merge migrations
duncte123 Apr 13, 2024
590411c
Set min frac digits to 0 when max is also 0
duncte123 Apr 14, 2024
5c0927e
Merge upstream
duncte123 Jul 29, 2024
ff37f34
Merge remote-tracking branch 'upstream/master' into feat/euros
duncte123 Aug 29, 2024
34e8489
Update from master
duncte123 Aug 29, 2024
1aaed51
Merge remote-tracking branch 'upstream/master' into feat/euros
duncte123 Oct 12, 2024
468cc1b
Merge database migrations
duncte123 Oct 14, 2024
4a0aadd
Merge branch 'master' into feat/euros
duncte123 Oct 24, 2024
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
5 changes: 4 additions & 1 deletion bundles/admin/donationProcessing/processDonations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import useSafeDispatch from '@public/api/useDispatch';
import { useCachedCallback } from '@public/hooks/useCachedCallback';
import { useFetchDonors } from '@public/hooks/useFetchDonors';
import Spinner from '@public/spinner';
import * as CurrencyUtils from '@public/util/currency';

import styles from './donations.mod.css';

Expand Down Expand Up @@ -156,7 +157,9 @@ export default React.memo(function ProcessDonations() {
{canEditDonors ? <a href={`${ADMIN_ROOT}donor/${donation.donor}`}>{donorLabel}</a> : donorLabel}
</td>
<td>
<a href={`${ADMIN_ROOT}donation/${donation.pk}`}>${(+donation.amount).toFixed(2)}</a>
<a href={`${ADMIN_ROOT}donation/${donation.pk}`}>
{CurrencyUtils.asCurrency(donation.amount, { currency: donation.currency })}
</a>
</td>
<td className={styles['comment']}>{donation.comment}</td>
<td>
Expand Down
3 changes: 3 additions & 0 deletions bundles/admin/donationProcessing/processDonationsSpec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('ProcessDonations', () => {
{
donor: 1,
amount: 164.87,
currency: 'USD',
comment: 'Amazing Comment',
pk: 123,
},
Expand Down Expand Up @@ -99,6 +100,7 @@ describe('ProcessDonations', () => {
{
donor: 1,
amount: 164.87,
currency: 'USD',
comment: 'Amazing Comment',
pk: 123,
},
Expand Down Expand Up @@ -190,6 +192,7 @@ describe('ProcessDonations', () => {
{
donor: 1,
amount: 164.87,
currency: 'USD',
comment: 'Amazing Comment',
pk: 123,
},
Expand Down
5 changes: 4 additions & 1 deletion bundles/admin/donationProcessing/readDonations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useSafeDispatch from '@public/api/useDispatch';
import { useCachedCallback } from '@public/hooks/useCachedCallback';
import { useFetchDonors } from '@public/hooks/useFetchDonors';
import Spinner from '@public/spinner';
import * as CurrencyUtils from '@public/util/currency';

import styles from './donations.mod.css';

Expand Down Expand Up @@ -114,7 +115,9 @@ export default React.memo(function ReadDonations() {
{canEditDonors ? <a href={`${ADMIN_ROOT}donor/${donation.donor}`}>{donorLabel}</a> : donorLabel}
</td>
<td>
<a href={`${ADMIN_ROOT}donation/${donation.pk}`}>${(+donation.amount).toFixed(2)}</a>
<a href={`${ADMIN_ROOT}donation/${donation.pk}`}>
{CurrencyUtils.asCurrency(donation.amount, { currency: donation.currency })}
</a>
</td>
<td className={styles['comment']}>
{donation.pinned && '📌'}
Expand Down
2 changes: 2 additions & 0 deletions bundles/admin/donationProcessing/readDonationsSpec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe('ReadDonations', () => {
{
donor: 1,
amount: 164.87,
currency: 'USD',
comment: 'Amazing Comment',
pinned: false,
pk: 123,
Expand Down Expand Up @@ -141,6 +142,7 @@ describe('ReadDonations', () => {
{
donor: 1,
amount: 164.87,
currency: 'USD',
comment: 'Amazing Comment',
pinned: true,
pk: 123,
Expand Down
9 changes: 7 additions & 2 deletions bundles/processing/modules/donations/DonationRow.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as React from 'react';
import classNames from 'classnames';
import { useDrag, useDrop } from 'react-dnd';
import { useSelector } from 'react-redux';
import { Clickable, Stack, Text } from '@spyrothon/sparx';

import type { Donation, DonationBid } from '@public/apiv2/APITypes';
import * as CurrencyUtils from '@public/util/currency';
import DragHandle from '@uikit/icons/DragHandle';

import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore';

import HighlightKeywords from './HighlightKeywords';

import styles from './DonationRow.mod.css';
Expand All @@ -19,9 +22,10 @@ interface BidsRowProps {

function BidsRow(props: BidsRowProps) {
const { bids } = props;
const currency = useSelector(EventDetailsStore.getEventCurrency);
duncte123 marked this conversation as resolved.
Show resolved Hide resolved
if (bids.length === 0) return null;

const bidNames = bids.map(bid => `${bid.bid_name} (${CurrencyUtils.asCurrency(bid.amount)})`);
const bidNames = bids.map(bid => `${bid.bid_name} (${CurrencyUtils.asCurrency(bid.amount, { currency })})`);

return (
<Text variant="text-sm/normal" className={styles.bids}>
Expand Down Expand Up @@ -76,7 +80,8 @@ export default function DonationRow(props: DonationRowProps) {
canDrop: checkDrop,
} = props;

const amount = CurrencyUtils.asCurrency(donation.amount);
const currency = useSelector(EventDetailsStore.getEventCurrency);
const amount = CurrencyUtils.asCurrency(donation.amount, { currency });
const donationTitle = (
<Text variant="header-sm/normal">
<strong>{amount}</strong>
Expand Down
10 changes: 7 additions & 3 deletions bundles/processing/modules/donations/ModCommentModal.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import * as React from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { Button, Card, FormControl, Header, Stack, Text, TextArea } from '@spyrothon/sparx';

import APIClient from '@public/apiv2/APIClient';
import { Donation } from '@public/apiv2/APITypes';
import * as CurrencyUtils from '@public/util/currency';
import TimeUtils from '@public/util/TimeUtils';

import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore';

import RelativeTime from '../time/RelativeTime';
import { useDonation } from './DonationsStore';

import styles from '../donation-groups/CreateEditDonationGroupModal.mod.css';

function renderDonationHeader(donation: Donation) {
function renderDonationHeader(donation: Donation, currency: string) {
const timestamp = TimeUtils.parseTimestamp(donation.timereceived);
const amount = CurrencyUtils.asCurrency(donation.amount);
const amount = CurrencyUtils.asCurrency(donation.amount, { currency });

return (
<Stack spacing="space-sm">
Expand Down Expand Up @@ -44,6 +47,7 @@ interface ModCommentModalProps {
export default function ModCommentModal(props: ModCommentModalProps) {
const { donationId, onClose } = props;

const currency = useSelector(EventDetailsStore.getEventCurrency);
const donation = useDonation(donationId);
const [comment, setComment] = React.useState(donation.modcomment ?? '');

Expand All @@ -59,7 +63,7 @@ export default function ModCommentModal(props: ModCommentModalProps) {
<Card floating className={styles.modal}>
<Stack as="form" spacing="space-lg" action="" onSubmit={handleSave}>
<Header tag="h1">Edit Mod Comment</Header>
<Card>{renderDonationHeader(donation)}</Card>
<Card>{renderDonationHeader(donation, currency)}</Card>
<FormControl label="Mod Comment">
<TextArea
value={comment}
Expand Down
6 changes: 5 additions & 1 deletion bundles/processing/modules/processing/ActionLog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { Anchor, Button, Header, Text } from '@spyrothon/sparx';

Expand All @@ -8,6 +9,8 @@ import { Donation } from '@public/apiv2/APITypes';
import * as CurrencyUtils from '@public/util/currency';
import Undo from '@uikit/icons/Undo';

import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore';

import { AdminRoutes, useAdminRoute } from '../../Routes';
import { loadDonations, useDonation } from '../donations/DonationsStore';
import useProcessingStore, { HistoryAction } from '../processing/ProcessingStore';
Expand All @@ -32,6 +35,7 @@ function getRelativeTime(timestamp: number, now: number = Date.now()) {
function ActionEntry({ action }: { action: HistoryAction }) {
const donationLink = useAdminRoute(AdminRoutes.DONATION(action.donationId));
const donation = useDonation(action.donationId);
const currency = useSelector(EventDetailsStore.getEventCurrency);

const store = useProcessingStore();
const unprocess = useMutation(
Expand All @@ -46,7 +50,7 @@ function ActionEntry({ action }: { action: HistoryAction }) {
},
);

const amount = CurrencyUtils.asCurrency(donation.amount);
const amount = CurrencyUtils.asCurrency(donation.amount, { currency });

return (
<div className={styles.action} key={action.id}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { Anchor, Button, Card, Checkbox, Header, openModal, Spacer, Stack, Text } from '@spyrothon/sparx';

import { usePermission } from '@public/api/helpers/auth';
Expand All @@ -8,6 +9,7 @@ import * as CurrencyUtils from '@public/util/currency';

import ModCommentModal from '@processing/modules/donations/ModCommentModal';
import { AdminRoutes, useAdminRoute } from '@processing/Routes';
import * as EventDetailsStore from '@tracker/event_details/EventDetailsStore';

import useDonationGroupsStore from '../donation-groups/DonationGroupsStore';
import { loadDonations, useDonation } from '../donations/DonationsStore';
Expand All @@ -21,8 +23,9 @@ export default function ReadingDonationRowPopout(props: ReadingDonationRowPopout
const { donationId, onClose } = props;
const donation = useDonation(donationId);
const { groups, addDonationToGroup, removeDonationFromGroup, removeDonationFromAllGroups } = useDonationGroupsStore();
const currency = useSelector(EventDetailsStore.getEventCurrency);

const amount = CurrencyUtils.asCurrency(donation.amount);
const amount = CurrencyUtils.asCurrency(donation.amount, { currency });
const donationLink = useAdminRoute(AdminRoutes.DONATION(donation.id));
const donorLink = useAdminRoute(AdminRoutes.DONOR(donation.donor));
const canEditDonors = usePermission('tracker.change_donor');
Expand Down
28 changes: 26 additions & 2 deletions bundles/public/util/currency.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
export function asCurrency(amount: string | number) {
return `$${Number(amount).toFixed(2)}`;
// This type enforces that consumers pass in the `currency` they want to display.
interface CurrencyOptions extends Omit<Intl.NumberFormatOptions, 'style'> {
currency: string;
}

export function asCurrency(amount: string | number, options: CurrencyOptions) {
// `en-US` is hardcoded here because we don't actually localize the frontend currently.
const formatter = new Intl.NumberFormat('en-US', { style: 'currency', ...options });

return formatter.format(Number(amount));
}

export function getCurrencySymbol(currency: string): string {
try {
const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency, currencyDisplay: 'narrowSymbol' });

for (const part of formatter.formatToParts(0)) {
if (part.type === 'currency') return part.value;
}
} catch (e: unknown) {
// Ignored: RangeError: invalid currency code in NumberFormat()
}

// If there was no currency symbol in the formatted string, then we can assume that
// the language does not expect there to be a symbol around the currency value.
return '';
}

export function parseCurrency(amount?: string) {
Expand Down
47 changes: 47 additions & 0 deletions bundles/public/util/currencySpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as CurrencyUtils from './currency';

describe('CurrencyUtils', () => {
describe('asCurrency', () => {
it('formats euros', () => {
const result = CurrencyUtils.asCurrency(123456.789, { currency: 'EUR', maximumFractionDigits: 0 });

expect(result).toEqual('€123,457');
});

it('formats with minimum digits', () => {
const result = CurrencyUtils.asCurrency(20, { currency: 'USD', minimumFractionDigits: 2 });

expect(result).toEqual('$20.00');
});

it('formats currency with full name', () => {
const result = CurrencyUtils.asCurrency(20, { currency: 'UAH', currencyDisplay: 'name' });

expect(result).toEqual('20.00 Ukrainian hryvnias');
});
});

describe('getCurrencySymbol', () => {
it('returns dollar symbol for USD', () => {
const result = CurrencyUtils.getCurrencySymbol('USD');

expect(result).toEqual('$');
});
it('returns euro symbol for EUR', () => {
const result = CurrencyUtils.getCurrencySymbol('EUR');

expect(result).toEqual('€');
});
it("returns input currency when it's 3 letters", () => {
const result = CurrencyUtils.getCurrencySymbol('GDQ');

// This is how the api works for some reason. GDQ is not a valid ISO-4217 currency, yet the api returns it.
expect(result).toEqual('GDQ');
});
it('returns blank string for wrong input', () => {
const result = CurrencyUtils.getCurrencySymbol('Invalid currency!');

expect(result).toEqual('');
});
});
});
27 changes: 15 additions & 12 deletions bundles/tracker/donation/__tests__/validateBid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('validateBid', () => {
amount: 2.5,
};

const validation = validateBid(bid, basicIncentive, donation, [], false, false);
const validation = validateBid('USD', bid, basicIncentive, donation, [], false, false);
expect(validation.valid).toBe(true);
expect(validation.errors.length).toEqual(0);
});
Expand All @@ -71,9 +71,12 @@ describe('validateBid', () => {
customoptionname: 'test',
};

const validation = validateBid(bid, basicIncentive, donation, [], false, false);
const validation = validateBid('USD', bid, basicIncentive, donation, [], false, false);
expect(validation.valid).toBe(false);
expect(validation.errors).toContain({ field: 'amount', message: BidErrors.AMOUNT_MINIMUM(BID_MINIMUM_AMOUNT) });
expect(validation.errors).toContain({
field: 'amount',
message: BidErrors.AMOUNT_MINIMUM(BID_MINIMUM_AMOUNT, 'USD'),
});
});

it('passes when amount equals allowed minimum', () => {
Expand All @@ -82,7 +85,7 @@ describe('validateBid', () => {
amount: BID_MINIMUM_AMOUNT,
};

const validation = validateBid(bid, basicIncentive, donation, [], false, false);
const validation = validateBid('USD', bid, basicIncentive, donation, [], false, false);
expect(validation.valid).toBe(true);
expect(validation.errors.length).toEqual(0);
});
Expand All @@ -94,7 +97,7 @@ describe('validateBid', () => {
amount: max,
};

const validation = validateBid(bid, basicIncentive, donation, [], false, false);
const validation = validateBid('USD', bid, basicIncentive, donation, [], false, false);
expect(validation.valid).toBe(true);
expect(validation.errors.length).toEqual(0);
});
Expand All @@ -106,9 +109,9 @@ describe('validateBid', () => {
amount: max + 1,
};

const validation = validateBid(bid, basicIncentive, donation, [], false, false);
const validation = validateBid('USD', bid, basicIncentive, donation, [], false, false);
expect(validation.valid).toBe(false);
expect(validation.errors).toContain({ field: 'amount', message: BidErrors.AMOUNT_MAXIMUM(max) });
expect(validation.errors).toContain({ field: 'amount', message: BidErrors.AMOUNT_MAXIMUM(max, 'USD') });
});
});

Expand All @@ -119,7 +122,7 @@ describe('validateBid', () => {
amount: 2.5,
};

const validation = validateBid(bid, incentiveWithOptions, donation, [], true, false);
const validation = validateBid('USD', bid, incentiveWithOptions, donation, [], true, false);
expect(validation.valid).toBe(false);
expect(validation.errors).toContain({ field: 'incentiveId', message: BidErrors.NO_CHOICE });
});
Expand All @@ -130,7 +133,7 @@ describe('validateBid', () => {
amount: 2.5,
};

const validation = validateBid(bid, incentiveWithOptions, donation, [], true, true);
const validation = validateBid('USD', bid, incentiveWithOptions, donation, [], true, true);
expect(validation.valid).toBe(true);
expect(validation.errors.length).toEqual(0);
});
Expand All @@ -142,7 +145,7 @@ describe('validateBid', () => {
customoptionname: 'test',
};

const validation = validateBid(bid, incentiveWithOptions, donation, [], true, true, true);
const validation = validateBid('USD', bid, incentiveWithOptions, donation, [], true, true, true);
expect(validation.valid).toBe(true);
expect(validation.errors.length).toEqual(0);
});
Expand All @@ -154,7 +157,7 @@ describe('validateBid', () => {
customoptionname: '',
};

const validation = validateBid(bid, incentiveWithOptions, donation, [], true, true, true);
const validation = validateBid('USD', bid, incentiveWithOptions, donation, [], true, true, true);
expect(validation.valid).toBe(false);
expect(validation.errors).toContain({
field: 'new option',
Expand All @@ -169,7 +172,7 @@ describe('validateBid', () => {
customoptionname: 'this is too long to be allowable clearly',
};

const validation = validateBid(bid, incentiveWithOptions, donation, [], true, true, true);
const validation = validateBid('USD', bid, incentiveWithOptions, donation, [], true, true, true);
expect(validation.valid).toBe(false);
expect(validation.errors).toContain({
field: 'new option',
Expand Down
Loading