Skip to content

Commit

Permalink
Merge pull request #49 from Prodeko:post-signup-migration
Browse files Browse the repository at this point in the history
Post-signup-migration
  • Loading branch information
ccruzkauppila authored Nov 5, 2024
2 parents 05187b2 + 4b30970 commit daac658
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
Warnings:
- You are about to drop the column `alreadyMigrated` on the `LegacyUser` table. All the data in the column will be lost.
- A unique constraint covering the columns `[newAccountId]` on the table `LegacyUser` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "LegacyUser" DROP COLUMN "alreadyMigrated",
ADD COLUMN "newAccountId" INTEGER;

-- CreateIndex
CREATE UNIQUE INDEX "LegacyUser_newAccountId_key" ON "LegacyUser"("newAccountId");

-- AddForeignKey
ALTER TABLE "LegacyUser" ADD CONSTRAINT "LegacyUser_newAccountId_fkey" FOREIGN KEY ("newAccountId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
10 changes: 6 additions & 4 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ model User {
Transactions Transaction[]
Deposit Deposit[]
WishLike WishLike[]
LegacyUser LegacyUser?
@@index([id])
}
Expand Down Expand Up @@ -161,10 +162,11 @@ model WishLike {
}

model LegacyUser {
id Int @unique
name String
balance Decimal
alreadyMigrated Boolean @default(false)
id Int @unique
name String
balance Decimal
newAccount User? @relation(fields: [newAccountId], references: [id])
newAccountId Int? @unique
}

enum WishStatus {
Expand Down
67 changes: 67 additions & 0 deletions src/app/(loggedin)/account/AccountMigrationDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useActionState, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";

import { AnimatedPopup, PopupRefActions } from "@/components/ui/AnimatedPopup";
import { FatButton } from "@/components/ui/Buttons/FatButton";
import { LineButton } from "@/components/ui/Buttons/LineButton";
import { DialogTitleWithButton } from "@/components/ui/DialogTitleWithButton";
import { MigrationCombobox } from "@/components/ui/MigrationCombobox";
import { migrateAccountAction } from "@/server/actions/account/migration";

export const AccountMigrationDialog = () => {
const [step, setStep] = useState(0);
const [result, formAction, isPending] = useActionState(migrateAccountAction, {
success: false,
error: undefined,
});

useEffect(() => {
if (result?.error) {
toast.error(result.error);
} else if (result?.success) {
toast.success("Account migrated successfully!");
closeModal();
}
}, [result]);
const popupRef = useRef<PopupRefActions>(undefined);
const closeModal = () => {
popupRef?.current?.closeContainer();
setStep(0);
};

const setupButton = (
<LineButton text="Migrate old account" buttonType="button" />
);

return (
<AnimatedPopup ref={popupRef} TriggerComponent={setupButton}>
<div className="flex flex-col items-center gap-4 px-6 py-6 md:gap-12 md:px-16 md:py-12">
<DialogTitleWithButton
title="Account migration"
onButtonClick={closeModal}
/>
<p className="text-md text-neutral-700 md:text-xl ">
Have an old Namukilke account you want to migrate? Use the form below
to move your old account's funds to this one. <br />
</p>
<p className="text-md text-red-400 md:text-xl">
Note that you can only migrate one account.
</p>
<form
action={formAction}
className="flex flex-col items-center gap-4 md:gap-12"
>
<MigrationCombobox />
<FatButton
buttonType="button"
type="submit"
text="Migrate account"
intent="primary"
loading={isPending}
fullwidth
/>
</form>
</div>
</AnimatedPopup>
);
};
12 changes: 11 additions & 1 deletion src/app/(loggedin)/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@ import { RfidSetupDialog } from "@/components/ui/RfidSetupDialog";
import { SectionTitle } from "@/components/ui/SectionTitle";
import { getCurrentUserBalance } from "@/server/actions/account/getBalance";
import { logoutAction } from "@/server/actions/auth/logout";
import { getCurrentUser } from "@/server/db/queries/account";
import {
getCurrentUser,
getCurrentUserMigrationStatus,
} from "@/server/db/queries/account";

import { AccountMigrationDialog } from "./AccountMigrationDialog";

const AccountPage = () => {
const pathName = usePathname();
const [nfcConnectionStatus, setNfcConnectionStatus] = useState<string | null>(
null,
);
const [userBalance, setUserBalance] = useState<string | null>(null);
const [userMigrated, setUserMigrated] = useState<boolean>(true);
useEffect(() => {
const checkNfcConnection = async () => {
const user = await getCurrentUser();
Expand All @@ -38,6 +44,9 @@ const AccountPage = () => {
getCurrentUserBalance().then((balance) => {
setUserBalance(formatCurrency(balance));
});
getCurrentUserMigrationStatus().then((migrated) => {
setUserMigrated(migrated);
});
}, []);
return (
<div className="flex h-full w-full flex-grow flex-col justify-between gap-6 bg-white py-8 md:py-12 ">
Expand Down Expand Up @@ -70,6 +79,7 @@ const AccountPage = () => {
</AddFundsDialog>

<RfidSetupDialog />
{!userMigrated && <AccountMigrationDialog />}
<LineButton
text="Purchase history"
buttonType="a"
Expand Down
21 changes: 21 additions & 0 deletions src/components/ui/DialogTitleWithButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { HiX } from "react-icons/hi";

interface Props {
title: string;
onButtonClick: () => void;
}

export const DialogTitleWithButton = ({ title, onButtonClick }: Props) => {
return (
<div className="flex w-full items-center justify-between md:-mb-12">
<p className="text-lg font-bold text-primary-400 md:text-3xl">{title}</p>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
<div
className="flex h-10 w-10 items-center justify-center rounded-full border-2 border-primary-400 bg-primary-50 text-2xl text-primary-400 md:h-16 md:w-16 md:border-2 md:text-4xl"
onClick={onButtonClick}
>
<HiX />
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion src/components/ui/MigrationCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const MigrationCombobox = () => {
? accountList
: accountList.filter(
(account) =>
account.name.includes(query) ||
account.name.toLowerCase().includes(query.toLowerCase()) ||
account.id.toString().includes(query),
);

Expand Down
59 changes: 58 additions & 1 deletion src/server/actions/account/migration.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
"use server";

import { revalidatePath } from "next/cache";

import { getSession } from "@/auth/ironsession";
import { ClientLegacyUser } from "@/common/types";
import { db } from "@/server/db/prisma";
import { newDeposit } from "@/server/db/queries/deposit";
import { ValueError } from "@/server/exceptions/exception";
import { Prisma, PrismaClient } from "@prisma/client";

export const getUnmigratedAccounts = async (): Promise<ClientLegacyUser[]> => {
const users = await db.legacyUser.findMany({
where: {
alreadyMigrated: false,
newAccountId: null,
},
});
return users.map((user) => ({
Expand All @@ -15,3 +21,54 @@ export const getUnmigratedAccounts = async (): Promise<ClientLegacyUser[]> => {
balance: user.balance.toNumber(),
}));
};

export const migrateAccountAction = async (
prevState: any,
formData: FormData,
) => {
try {
const session = await getSession();
const currentUserId = session?.user?.userId;
if (!currentUserId) {
throw new Error("Unauthorized");
}
const formUserId = formData.get("legacyAccountId") as string;
const legacyId = parseInt(formUserId);
if (!legacyId) {
throw new ValueError({
cause: "invalid_value",
message: "Couldn't find legacy user to migrate",
});
}

await db.$transaction(async (tx) => {
const legacyUser = await tx.legacyUser.update({
where: {
id: legacyId,
},
data: {
newAccountId: currentUserId,
},
});
if (!legacyUser) {
throw new ValueError({
cause: "invalid_value",
message: "Couldn't find legacy user to migrate",
});
}
const legacyBalance = legacyUser.balance.toNumber();
await newDeposit(tx as PrismaClient, currentUserId, legacyBalance);
});
revalidatePath("/account");
return { success: true };
} catch (error: any) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === "P2002") {
return {
error: "You've already migrated a legacy account!",
};
}
}
return { error: error?.message || "Unknown error occurred" };
}
};
51 changes: 37 additions & 14 deletions src/server/db/queries/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ export const createAccount = async ({
accountCredentials;
const pinHash = await createPincodeHash(pinCode);

const newUser = await client.user.create({
data: {
firstName,
lastName,
userName,
role,
pinHash,
},
});

let balance = 0;

if (legacyAccountId) {
Expand All @@ -41,7 +51,11 @@ export const createAccount = async ({
);
if (legacyUser) {
balance = legacyUser.balance.toNumber();
await migrateLegacyUser(client as PrismaClient, legacyAccountId);
await migrateLegacyUser(
client as PrismaClient,
legacyAccountId,
newUser.id,
);
} else {
throw new ValueError({
message: `Legacy user with id ${legacyAccountId} was not found`,
Expand All @@ -50,20 +64,13 @@ export const createAccount = async ({
}
}

return client.user.create({
await client.userBalance.create({
data: {
firstName,
lastName,
userName,
role,
pinHash,
Balances: {
create: {
balance,
},
},
userId: newUser.id,
balance,
},
});
return newUser;
};

export const getAllUsers = async () => {
Expand Down Expand Up @@ -189,13 +196,29 @@ export const getLegacyUserById = async (db: PrismaClient, legacyId: number) => {
});
};

export const migrateLegacyUser = async (db: PrismaClient, legacyId: number) => {
export const migrateLegacyUser = async (
db: PrismaClient,
legacyId: number,
newUserId: number,
) => {
await db.legacyUser.update({
where: {
id: legacyId,
},
data: {
alreadyMigrated: true,
newAccountId: newUserId,
},
});
};

export const getCurrentUserMigrationStatus = async () => {
const user = await getCurrentUser();
if (!user.ok) return false;
const legacyUser = await db.legacyUser.findFirst({
where: {
newAccountId: user.user.id,
},
});
console.log("got legacy user", legacyUser);
return !!legacyUser;
};

0 comments on commit daac658

Please sign in to comment.