Skip to content

Commit

Permalink
feat: switch to a bulk load strategy for sales from eActivities
Browse files Browse the repository at this point in the history
  • Loading branch information
Gum-Joe committed Oct 19, 2024
1 parent 630aedc commit a93e991
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 36 deletions.
6 changes: 5 additions & 1 deletion collection/app/(app)/PageActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ export const PageActions = ({
{...formHook.getInputProps("academicYears")}
/>
<Group>
<SyncEActivities setActionsError={setActionsError} />
<SyncEActivities
setActionsError={setActionsError}
academicYears={academicYears}
currentlySelectedAcademicYears={formHook.getValues().academicYears}
/>
<CSVImport
academicYears={academicYears}
currentAcademicYear={currentAcademicYear}
Expand Down
45 changes: 41 additions & 4 deletions collection/app/(app)/SyncEActivities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,41 @@ import { loadSalesFromEActivites } from "@/lib/crud/loadSalesFromEActivites";
import { fetcher } from "@/lib/fetcher";
import { StatusReturn } from "@/lib/types";
import { Product } from "@docsoc/eactivities";
import { Alert, Box, Button, Checkbox, Group, Loader, Modal, Stack, Text } from "@mantine/core";
import {
Alert,
Box,
Button,
Checkbox,
Group,
Loader,
Modal,
MultiSelect,
Stack,
Text,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { RootItem } from "@prisma/client";
import React, { useCallback, useState, useTransition } from "react";
import { FaSync } from "react-icons/fa";
import { FaUpRightFromSquare } from "react-icons/fa6";
import useSWR from "swr";

function CheckboxesForProducts({ close }: { close: () => void }) {
function CheckboxesForProducts({
close,
academicYears,
currentlySelectedAcademicYears,
}: {
close: () => void;
academicYears: string[];
currentlySelectedAcademicYears: string[];
}) {
const [value, setValue] = useState<string[]>([]);
const [status, setStatus] = useState<StatusReturn>({
status: "pending",
});

const [academicYearsToSync, setAcademicYearsToSync] = useState(currentlySelectedAcademicYears);

const { data: products, isLoading } = useSWR<RootItem[]>("/api/products/syncable", fetcher);

const [isPending, startTransition] = useTransition();
Expand All @@ -29,6 +50,7 @@ function CheckboxesForProducts({ close }: { close: () => void }) {
startTransition(async () => {
const res = await loadSalesFromEActivites(
value.map((id) => parseInt(id, 10)).filter(isFinite),
academicYearsToSync,
);
if (res.status === "error") {
setStatus(res);
Expand All @@ -39,7 +61,7 @@ function CheckboxesForProducts({ close }: { close: () => void }) {
close();
}
});
}, [close, value]);
}, [close, value, academicYearsToSync]);

const cards = products?.map((product, i) => (
<Checkbox.Card radius="md" value={product.id.toString(10)} key={i} flex="1 0 0">
Expand Down Expand Up @@ -88,6 +110,13 @@ function CheckboxesForProducts({ close }: { close: () => void }) {
</Alert>
)
}
<MultiSelect
label="Sync purchases these academic years"
description=" "
value={academicYearsToSync}
onChange={(e) => setAcademicYearsToSync(e)}
data={academicYears}
/>
<Checkbox.Group
value={value}
onChange={setValue}
Expand Down Expand Up @@ -119,15 +148,23 @@ function CheckboxesForProducts({ close }: { close: () => void }) {

export const SyncEActivities = ({
setActionsError,
academicYears,
currentlySelectedAcademicYears,
}: {
setActionsError: (error: string | null) => void;
academicYears: string[];
currentlySelectedAcademicYears: string[];
}) => {
const [opened, { open, close }] = useDisclosure(false);

return (
<>
<Modal opened={opened} onClose={close} title={`Select products to sync`} size="xl">
<CheckboxesForProducts close={close} />
<CheckboxesForProducts
close={close}
academicYears={academicYears}
currentlySelectedAcademicYears={currentlySelectedAcademicYears}
/>
</Modal>
<Button leftSection={<FaSync />} color="violet" onClick={open}>
Sync from eActivities
Expand Down
70 changes: 39 additions & 31 deletions collection/lib/crud/loadSalesFromEActivites.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use server";

import { auth } from "@/auth";
import { isValidAcademicYear, Sale } from "@docsoc/eactivities";
import { createLogger } from "@docsoc/util";
import { RootItem } from "@prisma/client";
import axios, { AxiosError } from "axios";
Expand All @@ -19,6 +20,7 @@ const logger = createLogger("collection.syncEActivities");
*/
export async function loadSalesFromEActivites(
productIdsFromDB: number[] = [],
academicYearsToSync: string[],
): Promise<StatusReturn> {
// Action so auth needed
const session = await auth();
Expand All @@ -30,6 +32,14 @@ export async function loadSalesFromEActivites(
};
}

// Validate academic years
if (!academicYearsToSync.every(isValidAcademicYear)) {
return {
status: "error",
error: "Invalid academic year in list to sync",
};
}

const eActivities = await getEactivities();

// 1: Find all products that have an eActivities ID
Expand Down Expand Up @@ -58,6 +68,30 @@ export async function loadSalesFromEActivites(
});
}

// 2: Fetch all sales for the academic years in question
// Doing simultaneously is tempting but would probably hit the API rate limit
const allSales: Sale[] = [];
for (const academicYear of academicYearsToSync) {
let salesReq: Awaited<ReturnType<typeof eActivities.getAllSales>>;
try {
salesReq = await eActivities.getAllSales(undefined, academicYear);
allSales.push(...salesReq.data);
} catch (e) {
if (axios.isAxiosError(e)) {
const message = e.response?.data?.Message ?? e?.message ?? e?.toString();
return {
status: "error",
error: `Failed to fetch sales for academic year ${academicYear} - ${message}.`,
};
} else {
return {
status: "error",
error: `Failed to fetch sales for academic year ${academicYear} - ${e?.toString()}.`,
};
}
}
}

// 0.1: Create import
// Name: <csv file name> DD/MM/YYYY HH:MM
const importName = `eActivities @ ${new Date().toLocaleString("en-GB")}`;
Expand All @@ -67,43 +101,17 @@ export async function loadSalesFromEActivites(
},
});

revalidatePath("/");
revalidatePath("/"); // so it shows up

// 2: For each product, fetch the sales from eActivities & insert
for (const product of products) {
// 2.1: Fetch the sales from eActivities
if (typeof product.eActivitiesId !== "number") {
logger.warn(`Product ${product.id} has an invalid eActivities ID, so skipping!`);
}
let salesReq: Awaited<ReturnType<typeof eActivities.getProductSales>>;
try {
salesReq = await eActivities.getProductSales(
undefined,
product.eActivitiesId as number,
);
} catch (e) {
if (axios.isAxiosError(e)) {
const message = e.response?.data?.Message ?? e?.message ?? e?.toString();
return {
status: "error",
error: `Failed to fetch sales for product ${product.id} - ${message}. Any imports up to this point have been preserved.`,
};
} else {
return {
status: "error",
error: `Failed to fetch sales for product ${
product.id
} - ${e?.toString()}. Any imports up to this point have been preserved.`,
};
}
}

if (salesReq.status !== 200) {
return {
status: "error",
error: `Failed to fetch sales for product ${product.id}: ${salesReq.status} with ${salesReq.statusText}`,
};
}

// Filter out sales
const sales = allSales.filter((sale) => sale.ProductID === product.eActivitiesId);

// Pull product data as well
let productData: Awaited<ReturnType<typeof eActivities.getProductById>>;
Expand Down Expand Up @@ -136,7 +144,7 @@ export async function loadSalesFromEActivites(
};
}
// 2.2: Insert the sales into the database
for (const sale of salesReq.data) {
for (const sale of sales) {
// 1: If user exists, get user
const user = await prisma.imperialStudent.upsert({
where: {
Expand Down

0 comments on commit a93e991

Please sign in to comment.