= {
20 | source: src,
21 | data,
22 | };
23 |
24 | netEventLogger.silly(`netPromise > ${eventName} > RequestObj`);
25 | netEventLogger.silly(promiseRequest);
26 |
27 | const promiseResp: PromiseEventResp = (data: ServerPromiseResp
) => {
28 | const endTime = process.hrtime.bigint();
29 | const totalTime = Number(endTime - startTime) / 1e6;
30 | emitNet(respEventName, src, data);
31 | netEventLogger.silly(`Response Promise Event ${respEventName} (${totalTime}ms), Data >>`);
32 | netEventLogger.silly(data);
33 | };
34 |
35 | // In case the cb is a promise, we use Promise.resolve
36 | Promise.resolve(cb(promiseRequest, promiseResp)).catch((e) => {
37 | netEventLogger.error(
38 | `An error occured for a onNetPromise (${eventName}), Error: ${e.message}`,
39 | );
40 |
41 | promiseResp({ status: 'error', errorMsg: 'UNKNOWN_ERROR' });
42 | });
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/web/src/utils/test.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/display-name */
2 | import React from 'react';
3 | import { render } from '@testing-library/react';
4 | import { Resource } from 'i18next';
5 | import { ReactElement, ReactNode, Suspense } from 'react';
6 | import { HashRouter, Router } from 'react-router-dom';
7 | import { createMemoryHistory, MemoryHistory } from 'history';
8 | import { createTheme, ThemeProvider } from '@mui/material';
9 | import { SnackbarProvider } from 'notistack';
10 |
11 | const theme = createTheme({
12 | palette: {
13 | mode: 'dark',
14 | },
15 | });
16 |
17 | const renderWithRouter = (history: MemoryHistory) => (ui: ReactNode) => {
18 | return {ui};
19 | };
20 |
21 | const renderWithTheme = (ui: ReactNode) => {
22 | return {ui};
23 | };
24 |
25 | const renderWithSuspense = (ui: ReactNode) => {
26 | return loading..}>{ui};
27 | };
28 |
29 | const renderWithSnackbar = (ui: ReactNode) => {
30 | return {ui};
31 | };
32 |
33 | type RenderWithProvidersOptions = {
34 | resources?: Resource;
35 | router?: Partial;
36 | history?: MemoryHistory;
37 | };
38 | export const renderWithProviders = (ui: ReactElement, options?: RenderWithProvidersOptions) => {
39 | const history = options?.history ?? createMemoryHistory();
40 |
41 | /* From bottom, to top. Lowest = rendered furthest out. */
42 | const providers = [
43 | renderWithRouter(history),
44 | renderWithSnackbar,
45 | renderWithSuspense,
46 | renderWithTheme,
47 | ];
48 |
49 | const renderedElement = providers.reduce((prevUi, provider) => {
50 | return provider(prevUi);
51 | }, ui);
52 |
53 | return render(renderedElement);
54 | };
55 |
--------------------------------------------------------------------------------
/src/server/decorators/Event.ts:
--------------------------------------------------------------------------------
1 | export const Event = (eventName: string) => {
2 | return function (target: object, key: string): void {
3 | if (!Reflect.hasMetadata('events', target)) {
4 | Reflect.defineMetadata('events', [], target);
5 | }
6 |
7 | const netEvents = Reflect.getMetadata('events', target) as Array;
8 |
9 | netEvents.push({
10 | eventName,
11 | key: key,
12 | net: false,
13 | });
14 |
15 | Reflect.defineMetadata('events', netEvents, target);
16 | };
17 | };
18 |
19 | export const NetEvent = (eventName: string) => {
20 | return function (target: any, key: string): void {
21 | if (!Reflect.hasMetadata('events', target)) {
22 | Reflect.defineMetadata('events', [], target);
23 | }
24 |
25 | const netEvents = Reflect.getMetadata('events', target) as Array;
26 |
27 | netEvents.push({
28 | eventName,
29 | key: key,
30 | net: true,
31 | });
32 |
33 | Reflect.defineMetadata('events', netEvents, target);
34 | };
35 | };
36 |
37 | export const EventListener = function () {
38 | return function (constructor: T) {
39 | return class extends constructor {
40 | constructor(...args: any[]) {
41 | super(...args);
42 |
43 | if (!Reflect.hasMetadata('events', this)) {
44 | Reflect.defineMetadata('events', [], this);
45 | }
46 |
47 | const events = Reflect.getMetadata('events', this) as Array;
48 |
49 | for (const { net, eventName, key } of events) {
50 | if (net)
51 | onNet(eventName, (...args: any[]) => {
52 | this[key](...args);
53 | });
54 | else
55 | on(eventName, (...args: any[]) => {
56 | this[key](...args);
57 | });
58 | }
59 | }
60 | };
61 | };
62 | };
63 |
--------------------------------------------------------------------------------
/src/client/lua/interaction.lua:
--------------------------------------------------------------------------------
1 | local config = json.decode(LoadResourceFile(GetCurrentResourceName(), "config.json"))
2 | local bank_coords = config.bankBlips.coords
3 | local atm_props = config.atms.props
4 |
5 | function display_help_text(text)
6 | BeginTextCommandDisplayHelp("STRING")
7 | AddTextComponentString(text)
8 | EndTextCommandDisplayHelp(0, false, false, -1)
9 | end
10 |
11 | CreateThread(function ()
12 |
13 | if not config.target.enabled then
14 | while true do
15 | local player_id = PlayerPedId()
16 | local player_coords = GetEntityCoords(player_id)
17 | local sleep = 1000
18 |
19 | for i = 1, #bank_coords do
20 | local pos = bank_coords[i]
21 |
22 | local distBank = #(player_coords - vector3(pos.x, pos.y, pos.z))
23 | if distBank <= 10.0 then
24 | DrawMarker(2, pos.x, pos.y, pos.z, 0.0, 0.0, 0.0, 0.0, 180.0, 0.0, 0.5, 0.5, 0.3, 255, 255, 255, 50, false, true, 2, nil, nil, false)
25 |
26 | if distBank <= 3.5 then
27 | display_help_text("Open bank: ~INPUT_PICKUP~")
28 |
29 |
30 | if IsControlJustReleased(0, 38) then
31 | exports["pefcl"]:openBank()
32 | end
33 | end
34 |
35 | sleep = 0
36 | end
37 | end
38 |
39 | for i = 1, #atm_props do
40 | local prop = GetClosestObjectOfType(player_coords, 5.0, joaat(atm_props[i]), false, false, false)
41 | local pos = GetEntityCoords(prop)
42 | local distAtm = #(player_coords - pos)
43 |
44 | if distAtm <= 2.0 then
45 | display_help_text("Open atm: ~INPUT_PICKUP~")
46 |
47 | if IsControlJustReleased(0, 38) then
48 | exports["pefcl"]:openAtm()
49 | end
50 |
51 | sleep = 0
52 | end
53 | end
54 |
55 | Wait(sleep)
56 | end
57 | end
58 | end)
59 |
--------------------------------------------------------------------------------
/web/src/hooks/useI18n.ts:
--------------------------------------------------------------------------------
1 | import { Language } from '@utils/i18nResourceHelpers';
2 | import { i18n } from 'i18next';
3 | import updateLocale from 'dayjs/plugin/updateLocale';
4 | import localizedFormat from 'dayjs/plugin/localizedFormat';
5 | import dayjs from 'dayjs';
6 | import { useCallback, useEffect, useState } from 'react';
7 | import { loadPefclResources } from 'src/views/Mobile/i18n';
8 |
9 | dayjs.extend(updateLocale);
10 | dayjs.extend(localizedFormat);
11 |
12 | export const useI18n = (initialI18n: i18n, language: Language) => {
13 | const [i18n, setI18n] = useState();
14 |
15 | const changeLanguage = useCallback(
16 | (language: Language) => {
17 | if (!i18n) {
18 | throw new Error('Cannot change language before i18n has been loaded.');
19 | }
20 |
21 | /* Change language for i18n */
22 | i18n.changeLanguage(language);
23 |
24 | /* Import locale for DayJS, then update translations & set locale */
25 | import(`dayjs/locale/${language}.js`).then(() => {
26 | dayjs.locale(language);
27 | dayjs.updateLocale(language, {
28 | calendar: {
29 | lastDay: i18n.t('calendar.lastDay'),
30 | sameDay: i18n.t('calendar.sameDay'),
31 | nextDay: i18n.t('calendar.nextDay'),
32 | lastWeek: i18n.t('calendar.lastWeek'),
33 | nextWeek: i18n.t('calendar.nextWeek'),
34 | sameElse: i18n.t('calendar.sameElse'),
35 | },
36 | });
37 | });
38 | },
39 | [i18n],
40 | );
41 |
42 | useEffect(() => {
43 | if (i18n) {
44 | changeLanguage(language);
45 | }
46 | }, [changeLanguage, i18n, language]);
47 |
48 | useEffect(() => {
49 | const instance = initialI18n.cloneInstance();
50 | loadPefclResources(instance);
51 | setI18n(instance);
52 | }, [initialI18n]);
53 |
54 | return { i18n: i18n, changeLanguage };
55 | };
56 |
--------------------------------------------------------------------------------
/src/server/services/invoice/invoice.db.ts:
--------------------------------------------------------------------------------
1 | import { singleton } from 'tsyringe';
2 | import { CreateInvoiceInput, GetInvoicesInput, InvoiceStatus } from '@typings/Invoice';
3 | import { InvoiceModel } from './invoice.model';
4 | import { MS_TWO_WEEKS } from '@utils/constants';
5 | import { Transaction } from 'sequelize/types';
6 |
7 | @singleton()
8 | export class InvoiceDB {
9 | async getAllInvoices(): Promise {
10 | return await InvoiceModel.findAll();
11 | }
12 |
13 | async getAllReceivingInvoices(
14 | identifier: string,
15 | pagination: GetInvoicesInput,
16 | ): Promise {
17 | return await InvoiceModel.findAll({
18 | where: { toIdentifier: identifier },
19 | ...pagination,
20 | order: [['createdAt', 'DESC']],
21 | });
22 | }
23 |
24 | async getReceivedInvoicesCount(identifier: string): Promise {
25 | return await InvoiceModel.count({ where: { toIdentifier: identifier } });
26 | }
27 |
28 | async getUnpaidInvoicesCount(identifier: string): Promise {
29 | return await InvoiceModel.count({
30 | where: { toIdentifier: identifier, status: InvoiceStatus.PENDING },
31 | });
32 | }
33 |
34 | async getInvoiceById(id: number, transaction: Transaction): Promise {
35 | return await InvoiceModel.findOne({ where: { id }, transaction });
36 | }
37 |
38 | async createInvoice(input: CreateInvoiceInput): Promise {
39 | const expiresAt = input.expiresAt
40 | ? input.expiresAt
41 | : new Date(Date.now() + MS_TWO_WEEKS).toString();
42 |
43 | return await InvoiceModel.create({ ...input, expiresAt });
44 | }
45 |
46 | async payInvoice(invoiceId: number): Promise {
47 | const [result] = await InvoiceModel.update(
48 | { status: InvoiceStatus.PAID },
49 | { where: { id: invoiceId } },
50 | );
51 | return result;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/client/cl_integrations.ts:
--------------------------------------------------------------------------------
1 | import { setBankIsOpen, setAtmIsOpen } from 'client';
2 | import cl_config from 'cl_config';
3 | import { translations } from 'i18n';
4 | const exp = global.exports;
5 |
6 | const isTargetEnabled = cl_config.target?.enabled ?? false;
7 | const targetType = cl_config.target?.type ?? 'qtarget';
8 | const isTargetDebugEnabled = cl_config.target?.debug ?? false;
9 | const isTargetAvailable = GetResourceState(targetType) === 'started';
10 |
11 | if (isTargetEnabled && isTargetAvailable) {
12 | const bankZones = cl_config.target?.bankZones ?? [];
13 | const atmModels = cl_config.atms?.props ?? [];
14 |
15 | atmModels.forEach((model) => {
16 | exp[targetType]['AddTargetModel'](model, {
17 | options: [
18 | {
19 | event: 'pefcl:open:atm',
20 | icon: 'fas fa-money-bill-1-wave',
21 | label: 'ATM',
22 | },
23 | ],
24 | });
25 | });
26 |
27 | bankZones.forEach((zone, index) => {
28 | const name = 'bank_' + index;
29 |
30 | if (!zone) {
31 | throw new Error('Missing zone. Check your "qtarget.bankZones" config.');
32 | }
33 |
34 | exp[targetType]['AddBoxZone'](
35 | name,
36 | zone.position,
37 | zone.length,
38 | zone.width,
39 | {
40 | name,
41 | heading: zone.heading,
42 | debugPoly: isTargetDebugEnabled,
43 | minZ: zone.minZ,
44 | maxZ: zone.maxZ,
45 | },
46 | {
47 | options: [
48 | {
49 | event: 'pefcl:open:bank',
50 | icon: 'fas fa-building-columns',
51 | label: translations.t('Open bank'),
52 | },
53 | ],
54 | distance: 1.5,
55 | },
56 | );
57 | });
58 |
59 | AddEventHandler('pefcl:open:atm', () => {
60 | setAtmIsOpen(true);
61 | });
62 |
63 | AddEventHandler('pefcl:open:bank', () => {
64 | setBankIsOpen(true);
65 | });
66 | }
67 |
--------------------------------------------------------------------------------
/src/client/functions.ts:
--------------------------------------------------------------------------------
1 | import { BalanceErrors } from '@typings/Errors';
2 | import API from 'cl_api';
3 | import { getNearestPlayer, validateAmount } from 'cl_utils';
4 |
5 | export const giveCash = async (_source: number, args: string[]) => {
6 | const [amount] = args;
7 |
8 | const isValid = validateAmount(amount);
9 | if (!isValid) {
10 | console.log('Invalid amount');
11 | return;
12 | }
13 |
14 | const nearestPlayer = getNearestPlayer(5);
15 | if (!nearestPlayer) {
16 | console.log('No player nearby.');
17 | return;
18 | }
19 |
20 | await API.giveCash(nearestPlayer.source, Number(amount)).catch((error: Error) => {
21 | if (error.message === BalanceErrors.InsufficentFunds) {
22 | console.log('You are too poor');
23 | return;
24 | }
25 |
26 | console.log(error);
27 | });
28 | };
29 |
30 | export const createInvoice = async (_source: number, args: string[]) => {
31 | const [amount, message] = args;
32 | const isValid = validateAmount(amount);
33 |
34 | if (!isValid) {
35 | console.log('Invalid amount');
36 | return;
37 | }
38 |
39 | const nearestPlayer = getNearestPlayer(5);
40 | if (!nearestPlayer) {
41 | console.log('No player nearby.');
42 | return;
43 | }
44 |
45 | await API.createInvoice({
46 | amount: Number(amount),
47 | message,
48 | source: nearestPlayer.source,
49 | });
50 | };
51 |
52 | export const depositMoney = async (amount: number) => {
53 | const isValid = validateAmount(amount);
54 |
55 | if (!isValid) {
56 | console.log('Invalid amount');
57 | return;
58 | }
59 |
60 | return await API.depositMoney(Number(amount));
61 | };
62 |
63 | export const withdrawMoney = async (amount: number) => {
64 | const isValid = validateAmount(amount);
65 |
66 | if (!isValid) {
67 | console.log('Invalid amount');
68 | return;
69 | }
70 |
71 | return await API.withdrawMoney(Number(amount));
72 | };
73 |
--------------------------------------------------------------------------------
/web/src/data/transactions.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'jotai';
2 | import { TransactionEvents } from '@typings/Events';
3 | import { GetTransactionsInput, GetTransactionsResponse } from '@typings/Transaction';
4 | import { mockedTransactions } from '../utils/constants';
5 | import { fetchNui } from '../utils/fetchNui';
6 | import { isEnvBrowser } from '../utils/misc';
7 |
8 | const initialState: GetTransactionsResponse = {
9 | total: 0,
10 | offset: 0,
11 | limit: 10,
12 | transactions: [],
13 | };
14 |
15 | const getTransactions = async (input: GetTransactionsInput): Promise => {
16 | try {
17 | const res = await fetchNui(TransactionEvents.Get, input);
18 | return res ?? initialState;
19 | } catch (e) {
20 | if (isEnvBrowser()) {
21 | return mockedTransactions;
22 | }
23 | console.error(e);
24 | return initialState;
25 | }
26 | };
27 |
28 | export const rawTransactionsAtom = atom(initialState);
29 |
30 | export const transactionBaseAtom = atom(
31 | async (get) => {
32 | const hasTransactions = get(rawTransactionsAtom).transactions.length > 0;
33 | return hasTransactions ? get(rawTransactionsAtom) : await getTransactions({ ...initialState });
34 | },
35 | async (get, set, by: Partial | undefined) => {
36 | const currentSettings = get(rawTransactionsAtom);
37 | return set(rawTransactionsAtom, await getTransactions({ ...currentSettings, ...by }));
38 | },
39 | );
40 |
41 | export const transactionsAtom = atom(async (get) => {
42 | const transactions = get(transactionBaseAtom).transactions;
43 | return transactions;
44 | });
45 |
46 | export const transactionsTotalAtom = atom((get) => get(transactionBaseAtom).total);
47 | export const transactionsLimitAtom = atom((get) => get(transactionBaseAtom).limit);
48 | export const transactionsOffsetAtom = atom((get) => get(transactionBaseAtom).offset);
49 |
--------------------------------------------------------------------------------
/web/src/components/ui/Fields/TextField.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { InputBase, InputBaseProps, StandardTextFieldProps, Typography } from '@mui/material';
3 | import React from 'react';
4 | import theme from '../../../utils/theme';
5 | import { Heading5 } from '../Typography/Headings';
6 |
7 | const Container = styled.div`
8 | display: flex;
9 | padding: 0.75rem 1rem;
10 | border-radius: ${theme.spacing(1)};
11 | background-color: ${theme.palette.background.dark12};
12 |
13 | & > div {
14 | flex: 1;
15 | }
16 |
17 | input:-webkit-autofill,
18 | input:-webkit-autofill:hover,
19 | input:-webkit-autofill:focus,
20 | input:-webkit-autofill:active {
21 | color: white !important;
22 | -webkit-text-fill-color: white !important;
23 | box-shadow: 0 0 0 30px rgb(16 26 37) inset !important;
24 | -webkit-box-shadow: 0 0 0 30px rgb(16 26 37) inset !important;
25 | }
26 | `;
27 |
28 | const LabelWrapper = styled.div`
29 | display: flex;
30 | flex-direction: column;
31 | `;
32 |
33 | const Label = styled(Heading5)`
34 | margin-bottom: 0.5rem;
35 | `;
36 |
37 | const HelperText = styled(Typography)`
38 | margin-top: 0.5rem;
39 | `;
40 |
41 | interface Props extends InputBaseProps {
42 | label?: string;
43 | helperText?: string;
44 | InputProps?: StandardTextFieldProps['InputProps'];
45 | InputLabelProps?: StandardTextFieldProps['InputLabelProps'];
46 | }
47 | const TextField = ({ InputProps, InputLabelProps, helperText, ...props }: Props) => {
48 | return (
49 |
50 | {props.label && }
51 |
52 |
53 |
54 |
55 | {helperText && (
56 |
57 | {helperText}
58 |
59 | )}
60 |
61 | );
62 | };
63 |
64 | export default TextField;
65 |
--------------------------------------------------------------------------------
/src/server/utils/frameworkIntegration.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FrameworkIntegrationExports,
3 | FrameworkIntegrationFunction,
4 | } from '@server/../../typings/exports';
5 | import { mainLogger } from '@server/sv_logger';
6 | import { getExports } from './misc';
7 | import { config } from './server-config';
8 |
9 | const log = mainLogger.child({ module: 'frameworkIntegration' });
10 |
11 | const frameworkIntegrationKeys: FrameworkIntegrationFunction[] = [
12 | 'addCash',
13 | 'removeCash',
14 | 'getCash',
15 | 'getBank',
16 | ];
17 |
18 | export const validateResourceExports = (resourceExports: FrameworkIntegrationExports): boolean => {
19 | let isValid = true;
20 | frameworkIntegrationKeys.forEach((key: FrameworkIntegrationFunction) => {
21 | if (typeof resourceExports[key] === 'undefined') {
22 | log.error(`Framework integration export ${key} is missing.`);
23 | isValid = false;
24 | return;
25 | }
26 |
27 | if (typeof resourceExports[key] !== 'function') {
28 | log.error(`Framework integration export ${key} is not a function.`);
29 | isValid = false;
30 | }
31 | });
32 |
33 | return isValid;
34 | };
35 |
36 | export const getFrameworkExports = (): FrameworkIntegrationExports => {
37 | const exps = getExports();
38 | const resourceName = config?.frameworkIntegration?.resource;
39 | const resourceExports: FrameworkIntegrationExports = exps[resourceName ?? ''];
40 |
41 | log.debug(`Checking exports from resource: ${resourceName}`);
42 |
43 | if (!resourceName) {
44 | log.error(`Missing resourceName in the config for framework integration`);
45 | throw new Error('Framework integration failed');
46 | }
47 |
48 | if (!resourceExports) {
49 | log.error(
50 | `No resource found with name: ${resourceName}. Make sure you have the correct resource name in the config.`,
51 | );
52 | throw new Error('Framework integration failed');
53 | }
54 |
55 | return resourceExports;
56 | };
57 |
--------------------------------------------------------------------------------
/src/server/services/account/account.model.ts:
--------------------------------------------------------------------------------
1 | import { DATABASE_PREFIX } from '@utils/constants';
2 | import { DataTypes, Model, Optional } from 'sequelize';
3 | import { config } from '@utils/server-config';
4 | import { Account, AccountRole, AccountType } from '@typings/Account';
5 | import { sequelize } from '@utils/pool';
6 | import { generateAccountNumber } from '@utils/misc';
7 | import { timestamps } from '../timestamps.model';
8 | import { AccountEvents } from '@server/../../typings/Events';
9 |
10 | export class AccountModel extends Model<
11 | Account,
12 | Optional
13 | > {}
14 |
15 | AccountModel.init(
16 | {
17 | id: {
18 | type: DataTypes.INTEGER,
19 | autoIncrement: true,
20 | primaryKey: true,
21 | },
22 | number: {
23 | type: DataTypes.STRING,
24 | unique: true,
25 | defaultValue: generateAccountNumber,
26 | },
27 | accountName: {
28 | type: DataTypes.STRING,
29 | validate: {
30 | max: 25,
31 | min: 1,
32 | },
33 | },
34 | isDefault: {
35 | type: DataTypes.BOOLEAN,
36 | defaultValue: false,
37 | },
38 | ownerIdentifier: {
39 | type: DataTypes.STRING,
40 | },
41 | role: {
42 | type: DataTypes.STRING,
43 | defaultValue: AccountRole.Owner,
44 | },
45 | balance: {
46 | type: DataTypes.INTEGER,
47 | defaultValue: config?.accounts?.otherAccountStartBalance ?? 0,
48 | },
49 | type: {
50 | type: DataTypes.STRING,
51 | defaultValue: AccountType.Personal,
52 | },
53 | ...timestamps,
54 | },
55 | {
56 | sequelize: sequelize,
57 | tableName: DATABASE_PREFIX + 'accounts',
58 | paranoid: true,
59 | hooks: {
60 | afterSave: (instance, options) => {
61 | if (options.fields?.includes('balance')) {
62 | emit(AccountEvents.NewBalance, instance.toJSON());
63 | }
64 | },
65 | },
66 | },
67 | );
68 |
--------------------------------------------------------------------------------
/web/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import { CircularProgress } from '@mui/material';
3 | import { Box } from '@mui/system';
4 | import React from 'react';
5 | import { useTranslation } from 'react-i18next';
6 | import Sidebar from './Sidebar';
7 | import { Heading2, Heading5 } from './ui/Typography/Headings';
8 | import { AnimatePresence, motion } from 'framer-motion';
9 |
10 | const Container = styled.div`
11 | display: flex;
12 | position: relative;
13 | height: 100%;
14 | `;
15 |
16 | const Content = styled(motion.div)`
17 | padding: 2rem;
18 | flex: 1;
19 | height: 100%;
20 | overflow: hidden;
21 | `;
22 |
23 | const LoadingContainer = styled.div`
24 | display: flex;
25 | flex-direction: column;
26 | padding: 2rem;
27 | `;
28 |
29 | const pageVariants = {
30 | initial: {
31 | x: 0,
32 | y: 400,
33 | },
34 | in: {
35 | x: 0,
36 | y: 0,
37 | },
38 | out: {
39 | x: 100,
40 | y: -200,
41 | },
42 | };
43 |
44 | const Layout: React.FC<{ title?: string }> = ({ children, title }) => {
45 | const { t } = useTranslation();
46 | return (
47 |
48 |
49 |
50 |
57 | {title}
58 |
61 | {t('Loading {{name}} view ..', { name: title })}
62 |
63 |
64 |
65 |
66 | }
67 | >
68 | {children}
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default Layout;
77 |
--------------------------------------------------------------------------------
/typings/exports/server.ts:
--------------------------------------------------------------------------------
1 | import { ServerPromiseResp } from '../http';
2 |
3 | type ExportResponse = ServerPromiseResp;
4 | type ExportCallback = (result: ExportResponse) => void;
5 |
6 | export enum ServerExports {
7 | GetCash = 'getCash',
8 | AddCash = 'addCash',
9 | RemoveCash = 'removeCash',
10 | DepositCash = 'depositCash',
11 | WithdrawCash = 'withdrawCash',
12 |
13 | GetTotalBankBalance = 'getTotalBankBalance',
14 | GetTotalBankBalanceByIdentifier = 'getTotalBankBalanceByIdentifier',
15 | GetDefaultAccountBalance = 'getDefaultAccountBalance',
16 | GetBankBalanceByIdentifier = 'getBankBalanceByIdentifier',
17 | SetBankBalance = 'setBankBalance',
18 | SetBankBalanceByIdentifier = 'setBankBalanceByIdentifier',
19 | AddBankBalance = 'addBankBalance',
20 | AddBankBalanceByIdentifier = 'addBankBalanceByIdentifier',
21 | AddBankBalanceByNumber = 'addBankBalanceByNumber',
22 | RemoveBankBalance = 'removeBankBalance',
23 | RemoveBankBalanceByIdentifier = 'removeBankBalanceByIdentifier',
24 | RemoveBankBalanceByNumber = 'removeBankBalanceByNumber',
25 |
26 | PayInvoice = 'payInvoice',
27 | GetInvoices = 'getInvoices',
28 | CreateInvoice = 'createInvoice',
29 | GetUnpaidInvoices = 'getUnpaidInvoices',
30 |
31 | LoadPlayer = 'loadPlayer',
32 | UnloadPlayer = 'unloadPlayer',
33 |
34 | GetAccounts = 'getAccounts',
35 | GetAccountsByIdentifier = 'getAccountsByIdentifier',
36 |
37 | /* Can be utilised by jobs or similar */
38 | CreateUniqueAccount = 'createUniqueAccount',
39 | GetUniqueAccount = 'getUniqueAccount',
40 | AddUserToUniqueAccount = 'addUserToUniqueAccount',
41 | RemoveUserFromUniqueAccount = 'removeUserFromUniqueAccount',
42 | }
43 |
44 | export type WithdrawMoneyExport = (
45 | source: number,
46 | amount: number,
47 | callback: ExportCallback,
48 | ) => Promise;
49 |
50 | export type DepositMoneyExport = (
51 | source: number,
52 | amount: number,
53 | callback: ExportCallback,
54 | ) => Promise;
55 |
--------------------------------------------------------------------------------
/src/server/utils/__tests__/misc.test.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_CLEARING_NUMBER } from '@utils/constants';
2 | import { generateAccountNumber, getClearingNumber } from '@utils/misc';
3 | import { createMockedConfig } from '@utils/test';
4 | import { regexExternalNumber } from '@shared/utils/regexes';
5 |
6 | const defaultValue = DEFAULT_CLEARING_NUMBER.toString();
7 | const clearingNumberConfig = (input: any) => {
8 | return createMockedConfig({ accounts: { clearingNumber: input } });
9 | };
10 |
11 | describe('Helper: getClearingNumber', () => {
12 | test('should take clearing number from config', () => {
13 | const config = clearingNumberConfig('900');
14 | expect(getClearingNumber(config)).toBe('900');
15 | });
16 |
17 | test('should handle number', () => {
18 | const config = clearingNumberConfig(900);
19 | expect(getClearingNumber(config)).toBe('900');
20 | });
21 |
22 | test('should default to 920', () => {
23 | expect(getClearingNumber()).toBe(defaultValue);
24 | });
25 |
26 | describe('error handling:', () => {
27 | test('Too long', () => {
28 | const config = clearingNumberConfig(9000);
29 | expect(getClearingNumber(config)).toBe(defaultValue);
30 | });
31 |
32 | test('Too short', () => {
33 | const config = clearingNumberConfig(90);
34 | expect(getClearingNumber(config)).toBe(defaultValue);
35 | });
36 |
37 | test('object', () => {
38 | const config = clearingNumberConfig({});
39 | expect(getClearingNumber(config)).toBe(defaultValue);
40 | });
41 |
42 | test('array', () => {
43 | const config = clearingNumberConfig([]);
44 | expect(getClearingNumber(config)).toBe(defaultValue);
45 | });
46 | });
47 | });
48 |
49 | describe('Helper: generateAccountNumber', () => {
50 | test('should pass regex test', () => {
51 | for (let i = 0; i < 100; i++) {
52 | const accountNumber = generateAccountNumber();
53 | expect(regexExternalNumber.test(accountNumber)).toBe(true);
54 | }
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/typings/config.ts:
--------------------------------------------------------------------------------
1 | export type DeepPartial = T extends object
2 | ? {
3 | [P in keyof T]?: DeepPartial;
4 | }
5 | : T;
6 |
7 | export type IdentifierType = 'license' | 'xbox' | 'discord' | 'steam';
8 |
9 | export interface PolyZone {
10 | position: {
11 | x: number;
12 | y: number;
13 | z: number;
14 | };
15 | length: number;
16 | width: number;
17 | heading: number;
18 | minZ: number;
19 | maxZ: number;
20 | }
21 |
22 | interface BlipCoords {
23 | x: number;
24 | y: number;
25 | z: number;
26 | }
27 | export interface ResourceConfig {
28 | general: {
29 | language: string;
30 | currency: string;
31 | identifierType: string;
32 | };
33 | frameworkIntegration: {
34 | enabled: boolean;
35 | resource: string;
36 | syncInitialBankBalance: boolean;
37 | };
38 | database: {
39 | profileQueries: boolean;
40 | shouldSync?: boolean;
41 | };
42 | prices: {
43 | newAccount: number;
44 | };
45 | accounts: {
46 | firstAccountStartBalance: number;
47 | otherAccountStartBalance: number;
48 | clearingNumber: string | number;
49 | maximumNumberOfAccounts: number;
50 | };
51 | cash: {
52 | startAmount: number;
53 | };
54 | atms: {
55 | distance: number;
56 | props: number[];
57 | withdrawOptions: number[];
58 | };
59 | bankBlips: {
60 | enabled: boolean;
61 | name: string;
62 | colour: number;
63 | icon: number;
64 | scale: number;
65 | display: number;
66 | shortRange: boolean;
67 | coords: BlipCoords[];
68 | };
69 | atmBlips: {
70 | enabled: boolean;
71 | name: string;
72 | colour: number;
73 | icon: number;
74 | scale: number;
75 | display: number;
76 | shortRange: boolean;
77 | coords: BlipCoords[];
78 | };
79 | target: {
80 | enabled: boolean;
81 | bankZones: PolyZone[];
82 | type: string;
83 | debug: boolean;
84 | };
85 | debug: {
86 | level: string;
87 | mockLicenses: boolean;
88 | };
89 | }
90 |
--------------------------------------------------------------------------------
/src/server/services/transaction/transaction.controller.ts:
--------------------------------------------------------------------------------
1 | import { TransactionEvents } from '@typings/Events';
2 | import { Request, Response } from '@typings/http';
3 | import {
4 | GetTransactionHistoryResponse,
5 | GetTransactionsInput,
6 | GetTransactionsResponse,
7 | CreateTransferInput,
8 | } from '@typings/Transaction';
9 | import { Controller } from '../../decorators/Controller';
10 | import { NetPromise, PromiseEventListener } from '../../decorators/NetPromise';
11 | import { TransactionService } from './transaction.service';
12 |
13 | @Controller('Transaction')
14 | @PromiseEventListener()
15 | export class TransactionController {
16 | private readonly _transactionService: TransactionService;
17 |
18 | constructor(transactionService: TransactionService) {
19 | this._transactionService = transactionService;
20 | }
21 |
22 | @NetPromise(TransactionEvents.Get)
23 | async getTransactions(
24 | req: Request,
25 | res: Response,
26 | ) {
27 | try {
28 | const transactions = await this._transactionService.handleGetMyTransactions(req);
29 | res({ status: 'ok', data: transactions });
30 | } catch (err) {
31 | res({ status: 'error', errorMsg: err.message });
32 | }
33 | }
34 |
35 | @NetPromise(TransactionEvents.CreateTransfer)
36 | async createTransfer(req: Request, res: Response