Skip to content

Commit 430d1d3

Browse files
authored
[Job Launcher] Billing system (#2485)
* Abuse system integration in Job Launcher server * Add job launcher server migrations * Create new component to setup card details * Merge branch 'develop' into feat/job-launcher-server/abuse * Create endpoints for the new billing flow: - Remove Old PaymentInfo Table and Store CustomerId in User Table - Update Endpoint to Create Customer and Add Payment Method - Create Endpoint to List Payment Methods - Create Endpoint to Remove a Payment Method - Create Endpoint to Retrieve User Billing Details - Create Endpoint to Edit Billing Details - Create Endpoint to Change Default Payment Method * Billing system implementation: - Modal to add card - Modal to delete card - Set as default button - Modal to select card - Edited fiat forms to use new billing system - Modal for billing details * Billing system in Job launcher client * Added unit tests for new methods * Comment abuse code and solve comments * Restore rate.service.ts change * remove lint error
1 parent 34269a2 commit 430d1d3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+4959
-367
lines changed

packages/apps/job-launcher/client/src/App.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import React from 'react';
2-
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
1+
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
32
import { ProtectedRoute } from './components/ProtectedRoute';
43
import Layout from './layouts';
54
import Dashboard from './pages/Dashboard';
@@ -8,7 +7,9 @@ import Home from './pages/Home';
87
import CreateJob from './pages/Job/CreateJob';
98
import JobDetail from './pages/Job/JobDetail';
109
import JobList from './pages/Job/JobList';
10+
import Settings from './pages/Profile/Settings';
1111
import TopUpAccount from './pages/Profile/TopUpAccount';
12+
import Transactions from './pages/Profile/Transactions';
1213
import ResetPassword from './pages/ResetPassword';
1314
import ValidateEmail from './pages/ValidateEmail';
1415
import VerifyEmail from './pages/VerifyEmail';
@@ -65,6 +66,22 @@ export default function App() {
6566
</ProtectedRoute>
6667
}
6768
/>
69+
<Route
70+
path="/profile/transactions"
71+
element={
72+
<ProtectedRoute>
73+
<Transactions />
74+
</ProtectedRoute>
75+
}
76+
/>
77+
<Route
78+
path="/profile/settings"
79+
element={
80+
<ProtectedRoute>
81+
<Settings />
82+
</ProtectedRoute>
83+
}
84+
/>
6885
<Route path="*" element={<Navigate to="/" />} />
6986
</Route>
7087
</Routes>
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import CloseIcon from '@mui/icons-material/Close';
2+
import { LoadingButton } from '@mui/lab';
3+
import {
4+
Box,
5+
Dialog,
6+
IconButton,
7+
MenuItem,
8+
TextField,
9+
Typography,
10+
useTheme,
11+
} from '@mui/material';
12+
import { useEffect, useState } from 'react';
13+
import { countryOptions, vatTypeOptions } from '../../constants/payment';
14+
import { useSnackbar } from '../../providers/SnackProvider';
15+
import { editUserBillingInfo } from '../../services/payment';
16+
import { BillingInfo } from '../../types';
17+
18+
const BillingDetailsModal = ({
19+
open,
20+
onClose,
21+
billingInfo,
22+
setBillingInfo,
23+
}: {
24+
open: boolean;
25+
onClose: () => void;
26+
billingInfo: BillingInfo;
27+
setBillingInfo: (value: BillingInfo) => void;
28+
}) => {
29+
const theme = useTheme();
30+
const [isLoading, setIsLoading] = useState(false);
31+
const [formData, setFormData] = useState<BillingInfo>(billingInfo);
32+
const [errors, setErrors] = useState<{ [key: string]: string }>({});
33+
const { showError } = useSnackbar();
34+
35+
useEffect(() => {
36+
if (billingInfo) {
37+
setFormData(billingInfo);
38+
}
39+
}, [billingInfo]);
40+
41+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
42+
const { name, value } = e.target;
43+
44+
if (['city', 'country', 'line', 'postalCode'].includes(name)) {
45+
setFormData((prevFormData) => ({
46+
...prevFormData,
47+
address: {
48+
...prevFormData.address,
49+
[name]: value,
50+
},
51+
}));
52+
} else {
53+
setFormData({
54+
...formData,
55+
[name]: value,
56+
});
57+
}
58+
};
59+
60+
const validateForm = () => {
61+
let newErrors: { [key: string]: string } = {};
62+
63+
if (!formData.name) {
64+
newErrors.name = 'name required';
65+
}
66+
67+
const addressFields = ['line', 'postalCode', 'city', 'country'];
68+
addressFields.forEach((field) => {
69+
if (!formData.address[field as keyof typeof formData.address]) {
70+
newErrors[field] = `${field} required`;
71+
}
72+
});
73+
74+
if (!formData.vat) {
75+
newErrors.vat = 'Tax ID required';
76+
}
77+
if (!formData.vatType) {
78+
newErrors.vatType = 'Tax ID type required';
79+
}
80+
81+
setErrors(newErrors);
82+
83+
return Object.keys(newErrors).length === 0;
84+
};
85+
86+
const handleSubmit = async () => {
87+
if (validateForm()) {
88+
setIsLoading(true);
89+
try {
90+
delete formData.email;
91+
await editUserBillingInfo(formData);
92+
setBillingInfo(formData);
93+
} catch (err: any) {
94+
showError(
95+
err.message || 'An error occurred while saving billing details.',
96+
);
97+
}
98+
setIsLoading(false);
99+
onClose();
100+
}
101+
};
102+
103+
return (
104+
<Dialog
105+
open={open}
106+
onClose={onClose}
107+
maxWidth={false}
108+
PaperProps={{ sx: { mx: 2, maxWidth: 'calc(100% - 32px)' } }}
109+
>
110+
<Box display="flex" maxWidth="950px">
111+
<Box
112+
width={{ xs: '0', md: '40%' }}
113+
display={{ xs: 'none', md: 'flex' }}
114+
sx={{
115+
background: theme.palette.primary.main,
116+
boxSizing: 'border-box',
117+
flexDirection: 'column',
118+
justifyContent: 'space-between',
119+
}}
120+
px={9}
121+
py={6}
122+
>
123+
<Typography variant="h4" fontWeight={600} color="#fff">
124+
{billingInfo ? 'Edit Billing Details' : 'Add Billing Details'}
125+
</Typography>
126+
</Box>
127+
<Box
128+
sx={{ boxSizing: 'border-box' }}
129+
width={{ xs: '100%', md: '60%' }}
130+
minWidth={{ xs: '340px', sm: '392px' }}
131+
display="flex"
132+
flexDirection="column"
133+
p={{ xs: 2, sm: 4 }}
134+
>
135+
<IconButton sx={{ ml: 'auto' }} onClick={onClose}>
136+
<CloseIcon color="primary" />
137+
</IconButton>
138+
139+
<Box width="100%" display="flex" flexDirection="column" gap={3}>
140+
<Typography variant="h6">Details</Typography>
141+
<TextField
142+
label="Name"
143+
name="name"
144+
value={formData.name}
145+
onChange={handleInputChange}
146+
fullWidth
147+
error={!!errors.name}
148+
helperText={errors.name}
149+
/>
150+
<TextField
151+
label="Address Line"
152+
name="line"
153+
value={formData.address.line}
154+
onChange={handleInputChange}
155+
fullWidth
156+
error={!!errors.line}
157+
helperText={errors.line || ''}
158+
/>
159+
<TextField
160+
label="Postal Code"
161+
name="postalCode"
162+
value={formData.address.postalCode}
163+
onChange={handleInputChange}
164+
fullWidth
165+
error={!!errors.postalCode}
166+
helperText={errors.postalCode || ''}
167+
/>
168+
<TextField
169+
label="City"
170+
name="city"
171+
value={formData.address.city}
172+
onChange={handleInputChange}
173+
fullWidth
174+
error={!!errors.city}
175+
helperText={errors.city || ''}
176+
/>
177+
<TextField
178+
select
179+
label="Country"
180+
name="country"
181+
value={formData.address.country}
182+
onChange={handleInputChange}
183+
fullWidth
184+
error={!!errors.country}
185+
helperText={errors.country || ''}
186+
>
187+
{Object.entries(countryOptions).map(([key, label]) => (
188+
<MenuItem key={key} value={key}>
189+
{label}
190+
</MenuItem>
191+
))}
192+
</TextField>
193+
194+
{/* VAT Section */}
195+
<Box display="flex" gap={2}>
196+
<TextField
197+
select
198+
label="Tax ID Type"
199+
name="vatType"
200+
value={formData.vatType || ''}
201+
onChange={handleInputChange}
202+
fullWidth
203+
error={!!errors.vatType}
204+
helperText={errors.vatType || ''}
205+
>
206+
{Object.entries(vatTypeOptions).map(([key, label]) => (
207+
<MenuItem key={key} value={key}>
208+
{label}
209+
</MenuItem>
210+
))}
211+
</TextField>
212+
213+
<TextField
214+
label="Tax ID Number"
215+
name="vat"
216+
value={formData.vat || ''}
217+
onChange={handleInputChange}
218+
fullWidth
219+
error={!!errors.vat}
220+
helperText={errors.vat || ''}
221+
/>
222+
</Box>
223+
<LoadingButton
224+
color="primary"
225+
variant="contained"
226+
fullWidth
227+
size="large"
228+
onClick={handleSubmit}
229+
loading={isLoading}
230+
>
231+
{billingInfo ? 'Save Changes' : 'Add Billing Details'}
232+
</LoadingButton>
233+
</Box>
234+
</Box>
235+
</Box>
236+
</Dialog>
237+
);
238+
};
239+
240+
export default BillingDetailsModal;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import CloseIcon from '@mui/icons-material/Close';
2+
import { Box, Dialog, IconButton, Typography, useTheme } from '@mui/material';
3+
import { CardSetupForm } from './CardSetupForm';
4+
5+
const AddCardModal = ({
6+
open,
7+
onClose,
8+
onComplete,
9+
}: {
10+
open: boolean;
11+
onClose: () => void;
12+
onComplete: () => void;
13+
}) => {
14+
const theme = useTheme();
15+
16+
const handleCardAdded = () => {
17+
onComplete();
18+
onClose();
19+
};
20+
21+
return (
22+
<Dialog
23+
open={open}
24+
onClose={onClose}
25+
maxWidth={false}
26+
PaperProps={{ sx: { mx: 2, maxWidth: 'calc(100% - 32px)' } }}
27+
>
28+
<Box display="flex" maxWidth="950px">
29+
<Box
30+
width={{ xs: '0', md: '40%' }}
31+
display={{ xs: 'none', md: 'flex' }}
32+
sx={{
33+
background: theme.palette.primary.main,
34+
boxSizing: 'border-box',
35+
flexDirection: 'column',
36+
justifyContent: 'space-between',
37+
}}
38+
px={9}
39+
py={6}
40+
>
41+
<Typography variant="h4" fontWeight={600} color="#fff">
42+
Add Credit Card Details
43+
</Typography>
44+
<Typography color="text.secondary" variant="caption">
45+
We need you to add a credit card in order to comply with HUMAN’s
46+
Abuse Mechanism. Learn more about it here.
47+
<br />
48+
<br />
49+
This card will be used for funding the jobs requested.
50+
</Typography>
51+
</Box>
52+
<Box
53+
sx={{ boxSizing: 'border-box' }}
54+
width={{ xs: '100%', md: '60%' }}
55+
minWidth={{ xs: '340px', sm: '392px' }}
56+
display="flex"
57+
flexDirection="column"
58+
p={{ xs: 2, sm: 4 }}
59+
>
60+
<IconButton sx={{ ml: 'auto' }} onClick={onClose}>
61+
<CloseIcon color="primary" />
62+
</IconButton>
63+
<Box width="100%" display="flex" flexDirection="column" gap={3}>
64+
<CardSetupForm onComplete={handleCardAdded} />
65+
</Box>
66+
</Box>
67+
</Box>
68+
</Dialog>
69+
);
70+
};
71+
72+
export default AddCardModal;

0 commit comments

Comments
 (0)