diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5622495..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index c88e743..9bfffd4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ node_modules/ # Yarn Integrity file .yarn-integrity + +.DS_Store \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2f56587..720e01f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.22.0", "@typescript-eslint/parser": "^8.22.0", - "eslint": "^9.28.0", + "eslint": "^9.30.0", "globals": "^15.14.0", "husky": "^9.1.7", "jest": "^29.7.0", @@ -2043,9 +2043,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2058,9 +2058,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2082,9 +2082,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", - "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2166,9 +2166,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.0.tgz", + "integrity": "sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==", "dev": true, "license": "MIT", "engines": { @@ -4588,9 +4588,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", "bin": { @@ -6518,19 +6518,19 @@ } }, "node_modules/eslint": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", - "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "version": "9.30.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.0.tgz", + "integrity": "sha512-iN/SiPxmQu6EVkf+m1qpBxzUhE12YqFLOSySuOyVLJLEF9nzTf+h/1AJYc1JWzCnktggeNrjvQGLngDzXirU6g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.28.0", + "@eslint/js": "9.30.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -6542,9 +6542,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -6579,9 +6579,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6620,9 +6620,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6646,15 +6646,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6664,9 +6664,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { diff --git a/package.json b/package.json index e4a9dd5..c14bc57 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.22.0", "@typescript-eslint/parser": "^8.22.0", - "eslint": "^9.28.0", + "eslint": "^9.30.0", "globals": "^15.14.0", "husky": "^9.1.7", "jest": "^29.7.0", diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 86d74fa..0000000 Binary files a/src/.DS_Store and /dev/null differ diff --git a/src/config/prisma.ts b/src/config/prisma.ts index 33d26c8..9ac94da 100644 --- a/src/config/prisma.ts +++ b/src/config/prisma.ts @@ -15,7 +15,7 @@ const prismaClientSingleton = () => { // Ensure we only create one instance of PrismaClient declare global { - // eslint-disable-next-line no-var + var prisma: PrismaClient | undefined; } diff --git a/src/entities/BaseEntity.ts b/src/entities/BaseEntity.ts deleted file mode 100644 index e813a47..0000000 --- a/src/entities/BaseEntity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; - -export abstract class BaseEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/entities/NFT.ts b/src/entities/NFT.ts deleted file mode 100644 index e71e65c..0000000 --- a/src/entities/NFT.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm'; -import { BaseEntity } from './BaseEntity'; -import { User } from './User'; -import { Organization } from './Organization'; - -@Entity() -export class NFT extends BaseEntity { - @ManyToOne(() => User, { nullable: false }) - @JoinColumn({ name: 'userId' }) - user: User; - - @ManyToOne(() => Organization, { nullable: false }) - @JoinColumn({ name: 'organizationId' }) - organization: Organization; - - @Column({ type: 'text', nullable: false }) - description: string; -} \ No newline at end of file diff --git a/src/entities/Organization.ts b/src/entities/Organization.ts deleted file mode 100644 index c675c7b..0000000 --- a/src/entities/Organization.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - OneToMany, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm"; -import { NFT } from "./NFT"; - -@Entity() -export class Organization { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - name: string; - - @Column({ unique: true }) - email: string; - - @Column({ nullable: true }) - description: string; - - @OneToMany(() => NFT, (nft) => nft.organization) - nfts: NFT[]; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/entities/Photo.ts b/src/entities/Photo.ts deleted file mode 100644 index e6c8252..0000000 --- a/src/entities/Photo.ts +++ /dev/null @@ -1,26 +0,0 @@ -export class Photo { - id?: string; - url: string; - userId: string; - uploadedAt?: Date; - metadata?: Record; - - constructor(props: any) { - this.id = props.id; - this.url = props.url; - this.userId = props.userId; - this.uploadedAt = props.uploadedAt; - this.metadata = props.metadata ?? {}; - } - - validate(): boolean { - if (!this.url) throw new Error('Photo URL is required'); - if (!/^https?:\/\/.+$/.test(this.url)) throw new Error('Photo URL is invalid'); - if (!this.userId) throw new Error('User ID is required'); - return true; - } - - updateMetadata(newMetadata: Record): void { - this.metadata = { ...this.metadata, ...newMetadata }; - } -} diff --git a/src/entities/Project.ts b/src/entities/Project.ts deleted file mode 100644 index bb87bb0..0000000 --- a/src/entities/Project.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity, Column, OneToMany } from "typeorm"; -import { BaseEntity } from "./BaseEntity"; -import { Volunteer } from "./Volunteer"; - -@Entity() -export class Project extends BaseEntity { - @Column({ type: "varchar", length: 255, nullable: false }) - name: string; - - @Column({ type: "text", nullable: false }) - description: string; - - @Column({ type: "varchar", length: 255, nullable: false }) - location: string; - - @Column({ type: "date", nullable: false }) - startDate: Date; - - @Column({ type: "date", nullable: false }) - endDate: Date; - - @Column({ type: "uuid", nullable: false }) - organizationId: string; - - @OneToMany(() => Volunteer, (volunteer) => volunteer.project) - volunteers?: Volunteer[]; -} diff --git a/src/entities/TestItem.ts b/src/entities/TestItem.ts deleted file mode 100644 index c425ab5..0000000 --- a/src/entities/TestItem.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Entity, Column } from 'typeorm'; -import { BaseEntity } from './BaseEntity'; - -@Entity() -export class TestItem extends BaseEntity { - @Column() - name: string; - - @Column() - value: number; - - @Column() - age: number; -} diff --git a/src/entities/User.ts b/src/entities/User.ts deleted file mode 100644 index b940838..0000000 --- a/src/entities/User.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Entity, Column, BaseEntity, PrimaryGeneratedColumn } from 'typeorm'; - -@Entity() -export class User extends BaseEntity { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column() - name: string; - - @Column() - lastName: string; - - @Column({ unique: true }) - email: string; - - @Column() - password: string; - - @Column({ unique: true }) - wallet: string; - - @Column({ default: false }) - isVerified: boolean; - - @Column({ nullable: true }) - verificationToken: string; - - @Column({ type: 'timestamp', nullable: true }) - verificationTokenExpires: Date; -} diff --git a/src/entities/Volunteer.ts b/src/entities/Volunteer.ts deleted file mode 100644 index d79b7c9..0000000 --- a/src/entities/Volunteer.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Entity, Column, JoinColumn, ManyToOne } from 'typeorm'; -import { BaseEntity } from './BaseEntity'; -import { Project } from './Project'; - -@Entity() -export class Volunteer extends BaseEntity { - @Column({ type: 'varchar', length: 255, nullable: false }) - name!: string; - - @Column({ type: 'varchar', length: 255, nullable: false }) - description!: string; - - @Column({ type: 'varchar', length: 255, nullable: false }) - requirements!: string; - - @Column({ nullable: true }) - incentive?: string; - - @ManyToOne(() => Project, (project) => project.volunteers, { - nullable: false, - }) - @JoinColumn({ name: 'projectId' }) - project!: Project; -} diff --git a/src/entities/userVolunteer.entity.ts b/src/entities/userVolunteer.entity.ts deleted file mode 100644 index d02d60b..0000000 --- a/src/entities/userVolunteer.entity.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Entity, PrimaryColumn, CreateDateColumn } from 'typeorm'; -import { BaseEntity } from './BaseEntity'; - -@Entity() -export class UserVolunteer extends BaseEntity { - @PrimaryColumn('uuid') - userId: string; // Simple reference field for now - - @PrimaryColumn('uuid') - volunteerId: string; // Simple reference field for now - - @CreateDateColumn() - joinedAt: Date; -} diff --git a/src/modules/.DS_Store b/src/modules/.DS_Store deleted file mode 100644 index 490f1ba..0000000 Binary files a/src/modules/.DS_Store and /dev/null differ diff --git a/src/modules/nft/__tests__/domain/entities/nft.entity.test.ts b/src/modules/nft/__tests__/domain/entities/nft.entity.test.ts new file mode 100644 index 0000000..41cd63e --- /dev/null +++ b/src/modules/nft/__tests__/domain/entities/nft.entity.test.ts @@ -0,0 +1,82 @@ +import { NFT } from "../../../domain/entities/nft.entity" +import { User } from "../../../../user/domain/entities/user.entity" +import { Organization } from "../../../../organization/domain/entities/organization.entity" + +describe("NFT Entity", () => { + let nft: NFT + let user: User + let organization: Organization + + beforeEach(() => { + user = new User() + user.id = "user-123" + user.name = "John" + user.lastName = "Doe" + user.email = "john@example.com" + + organization = new Organization({ + id: "org-123", + name: "Test Org", + email: "org@example.com", + description: "Test Organization Description", + isVerified: false + }) + + nft = new NFT() + nft.user = user + nft.userId = user.id + nft.organization = organization + nft.organizationId = organization.id + nft.description = "Test NFT Description" + nft.isMinted = false + }) + + describe("Creation", () => { + it("should create an NFT with valid properties", () => { + expect(nft.userId).toBe("user-123") + expect(nft.organizationId).toBe("org-123") + expect(nft.description).toBe("Test NFT Description") + expect(nft.isMinted).toBe(false) + }) + }) + + describe("Minting", () => { + it("should mint NFT with token details", () => { + const tokenId = "token-123" + const contractAddress = "0x123..." + const metadataUri = "https://metadata.uri" + + nft.mint(tokenId, contractAddress, metadataUri) + + expect(nft.tokenId).toBe(tokenId) + expect(nft.contractAddress).toBe(contractAddress) + expect(nft.metadataUri).toBe(metadataUri) + expect(nft.isMinted).toBe(true) + }) + + it("should throw error when minting already minted NFT", () => { + nft.isMinted = true + + expect(() => { + nft.mint("token-123", "0x123...") + }).toThrow("NFT is already minted") + }) + }) + + describe("Metadata Management", () => { + it("should update metadata URI", () => { + const newMetadataUri = "https://new-metadata.uri" + + nft.updateMetadata(newMetadataUri) + + expect(nft.metadataUri).toBe(newMetadataUri) + }) + }) + + describe("Ownership", () => { + it("should check if NFT is owned by user", () => { + expect(nft.isOwnedBy("user-123")).toBe(true) + expect(nft.isOwnedBy("other-user")).toBe(false) + }) + }) +}) diff --git a/src/modules/nft/domain/entities/nft.entity.ts b/src/modules/nft/domain/entities/nft.entity.ts index 953512b..944d243 100644 --- a/src/modules/nft/domain/entities/nft.entity.ts +++ b/src/modules/nft/domain/entities/nft.entity.ts @@ -1,9 +1,67 @@ -export class NFT { +import { Entity, Column, ManyToOne, JoinColumn } from "typeorm" +import { BaseEntity } from "../../../shared/domain/entities/base.entity" +import { Organization } from "../../../organization/domain/entities/organization.entity" +import { User } from "@/modules/user/domain/entities/User.entity" + +@Entity("nfts") +export class NFT extends BaseEntity { + @ManyToOne(() => User, { nullable: false }) + @JoinColumn({ name: "userId" }) + user: User + + @Column({ type: "uuid", nullable: false }) + userId: string + + @ManyToOne(() => Organization, { nullable: false }) + @JoinColumn({ name: "organizationId" }) + organization: Organization + + @Column({ type: "uuid", nullable: false }) + organizationId: string + + @Column({ type: "text", nullable: false }) + description: string + + @Column({ type: "varchar", length: 255, nullable: true }) + tokenId?: string + + @Column({ type: "varchar", length: 255, nullable: true }) + contractAddress?: string + + @Column({ type: "varchar", length: 500, nullable: true }) + metadataUri?: string + + @Column({ type: "boolean", default: false }) + isMinted: boolean + + // Domain methods + public mint(tokenId: string, contractAddress: string, metadataUri?: string): void { + if (this.isMinted) { + throw new Error("NFT is already minted") + } + + this.tokenId = tokenId + this.contractAddress = contractAddress + this.metadataUri = metadataUri + this.isMinted = true + } + + public updateMetadata(metadataUri: string): void { + this.metadataUri = metadataUri + } + + public isOwnedBy(userId: string): boolean { + return this.userId === userId + } +} + +// Keep the simple domain class for use cases +export class NFTDomain { constructor( public readonly id: string, public readonly userId: string, public readonly organizationId: string, public readonly description: string, - public readonly createdAt: Date + public readonly createdAt: Date, ) {} } diff --git a/src/modules/organization/domain/entities/organization.entity.ts b/src/modules/organization/domain/entities/organization.entity.ts index 4fba193..e193e70 100644 --- a/src/modules/organization/domain/entities/organization.entity.ts +++ b/src/modules/organization/domain/entities/organization.entity.ts @@ -1,6 +1,7 @@ import { BaseEntity } from "../../../shared/domain/entities/base.entity"; export interface OrganizationProps { + id: string; name: string; email: string; description: string; @@ -31,7 +32,7 @@ export class Organization extends BaseEntity { createdAt?: Date, updatedAt?: Date ) { - super(id, createdAt, updatedAt); + super(); this.name = props.name; this.email = props.email; this.description = props.description; @@ -51,6 +52,7 @@ export class Organization extends BaseEntity { public update(props: Partial): Organization { return new Organization( { + id: this.id, name: props.name ?? this.name, email: props.email ?? this.email, description: props.description ?? this.description, diff --git a/src/modules/photo/__tests__/domain/entities/photo.entity.test.ts b/src/modules/photo/__tests__/domain/entities/photo.entity.test.ts new file mode 100644 index 0000000..f7993a9 --- /dev/null +++ b/src/modules/photo/__tests__/domain/entities/photo.entity.test.ts @@ -0,0 +1,118 @@ +import { Photo } from "../../../domain/entities/photo.entity" + +describe("Photo Entity", () => { + const validPhotoProps = { + url: "https://example.com/photo.jpg", + userId: "user-123", + metadata: { size: 1024, format: "jpg" }, + } + + describe("Creation", () => { + it("should create a photo with valid props", () => { + const photo = Photo.create(validPhotoProps) + + expect(photo).toBeInstanceOf(Photo) + expect(photo.url).toBe(validPhotoProps.url) + expect(photo.userId).toBe(validPhotoProps.userId) + expect(photo.metadata).toEqual(validPhotoProps.metadata) + }) + + it("should create photo with empty metadata if not provided", () => { + const photo = Photo.create({ + url: validPhotoProps.url, + userId: validPhotoProps.userId, + }) + + expect(photo.metadata).toEqual({}) + }) + }) + + describe("Validation", () => { + it("should throw error if URL is empty", () => { + expect(() => { + Photo.create({ + ...validPhotoProps, + url: "", + }) + }).toThrow("Photo URL is required") + }) + + it("should throw error if URL is invalid", () => { + expect(() => { + Photo.create({ + ...validPhotoProps, + url: "invalid-url", + }) + }).toThrow("Photo URL must be a valid HTTP/HTTPS URL") + }) + + it("should throw error if userId is empty", () => { + expect(() => { + Photo.create({ + ...validPhotoProps, + userId: "", + }) + }).toThrow("User ID is required") + }) + + it("should accept valid HTTP and HTTPS URLs", () => { + expect(() => { + Photo.create({ + ...validPhotoProps, + url: "http://example.com/photo.jpg", + }) + }).not.toThrow() + + expect(() => { + Photo.create({ + ...validPhotoProps, + url: "https://example.com/photo.jpg", + }) + }).not.toThrow() + }) + }) + + describe("Metadata Management", () => { + it("should update metadata", () => { + const photo = Photo.create(validPhotoProps) + const newMetadata = { width: 800, height: 600 } + + photo.updateMetadata(newMetadata) + + expect(photo.metadata).toEqual({ + ...validPhotoProps.metadata, + ...newMetadata, + }) + }) + + it("should merge metadata without overwriting existing keys", () => { + const photo = Photo.create(validPhotoProps) + const additionalMetadata = { width: 800 } + + photo.updateMetadata(additionalMetadata) + + expect(photo.metadata).toEqual({ + size: 1024, + format: "jpg", + width: 800, + }) + }) + }) + + describe("ToObject", () => { + it("should convert entity to plain object", () => { + const photo = Photo.create(validPhotoProps) + const photoObject = photo.toObject() + + expect(photoObject).toEqual( + expect.objectContaining({ + url: validPhotoProps.url, + userId: validPhotoProps.userId, + metadata: validPhotoProps.metadata, + }), + ) + expect(photoObject.id).toBeTruthy() + expect(photoObject.uploadedAt).toBeInstanceOf(Date) + }) + }) +}) diff --git a/src/modules/photo/__tests__/photo.entity.test.ts b/src/modules/photo/__tests__/photo.entity.test.ts deleted file mode 100644 index 88b07c6..0000000 --- a/src/modules/photo/__tests__/photo.entity.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -// src/modules/photo/__tests__/domain/photo.entity.test.ts - -import { Photo } from "../entities/photo.entity"; - -describe("Photo Entity", () => { - const validPhotoProps = { - url: "https://example.com/photo.jpg", - userId: "user-123", - }; - - describe("Creation", () => { - it("should create a photo with valid props", () => { - const photo = Photo.create(validPhotoProps); - - expect(photo).toBeInstanceOf(Photo); - expect(photo.url).toBe(validPhotoProps.url); - expect(photo.userId).toBe(validPhotoProps.userId); - expect(photo.id).toBeDefined(); - expect(photo.uploadedAt).toBeInstanceOf(Date); - }); - - it("should accept optional metadata", () => { - const photoWithMetadata = Photo.create({ - ...validPhotoProps, - metadata: { size: "1024kb", format: "jpg" }, - }); - - expect(photoWithMetadata.metadata).toEqual({ size: "1024kb", format: "jpg" }); - }); - }); - - describe("Validation", () => { - it("should validate a photo with all required fields", () => { - const photo = new Photo(validPhotoProps); - expect(photo.validate()).toBe(true); - }); - - it("should throw an error when URL is missing", () => { - const photo = new Photo({ - ...validPhotoProps, - url: "", - }); - - expect(() => photo.validate()).toThrow("Photo URL is required"); - }); - - it("should throw an error when URL is invalid", () => { - const photo = new Photo({ - ...validPhotoProps, - url: "invalid-url", - }); - - expect(() => photo.validate()).toThrow("Photo URL must be a valid HTTP/HTTPS URL"); - }); - - it("should throw an error when userId is missing", () => { - const photo = new Photo({ - ...validPhotoProps, - userId: "", - }); - - expect(() => photo.validate()).toThrow("User ID is required"); - }); - }); - - describe("updateMetadata", () => { - it("should merge new metadata with existing metadata", () => { - const photo = Photo.create({ - ...validPhotoProps, - metadata: { size: "1024kb" }, - }); - - photo.updateMetadata({ format: "jpg" }); - expect(photo.metadata).toEqual({ size: "1024kb", format: "jpg" }); - }); - - it("should create metadata object if it was undefined", () => { - const photo = Photo.create(validPhotoProps); - photo.updateMetadata({ width: 800, height: 600 }); - - expect(photo.metadata).toEqual({ width: 800, height: 600 }); - }); - }); - - describe("toObject", () => { - it("should convert entity to plain object", () => { - const photo = Photo.create(validPhotoProps); - const plainObject = photo.toObject(); - - expect(plainObject).toEqual({ - id: photo.id, - url: photo.url, - userId: photo.userId, - uploadedAt: photo.uploadedAt, - metadata: undefined, - }); - }); - }); -}); \ No newline at end of file diff --git a/src/modules/photo/domain/entities/photo.entity.ts b/src/modules/photo/domain/entities/photo.entity.ts new file mode 100644 index 0000000..e9cab65 --- /dev/null +++ b/src/modules/photo/domain/entities/photo.entity.ts @@ -0,0 +1,68 @@ +import { Entity, Column } from "typeorm" +import { BaseEntity } from "../../../shared/domain/entities/base.entity" + +export interface PhotoProps { + id?: string + url: string + userId: string + uploadedAt?: Date + metadata?: Record +} + +@Entity("photos") +export class Photo extends BaseEntity { + @Column({ type: "varchar", length: 500, nullable: false }) + url: string + + @Column({ type: "uuid", nullable: false }) + userId: string + + @Column({ type: "jsonb", nullable: true }) + metadata?: Record + + // Domain logic and validation + public validate(): boolean { + if (!this.url || this.url.trim() === "") { + throw new Error("Photo URL is required") + } + + if (!/^https?:\/\/.+$/.test(this.url)) { + throw new Error("Photo URL must be a valid HTTP/HTTPS URL") + } + + if (!this.userId || this.userId.trim() === "") { + throw new Error("User ID is required") + } + + return true + } + + // Update metadata + public updateMetadata(newMetadata: Record): void { + this.metadata = { + ...this.metadata, + ...newMetadata, + } + } + + // Static factory method + public static create(props: PhotoProps): Photo { + const photo = new Photo() + photo.url = props.url + photo.userId = props.userId + photo.metadata = props.metadata ?? {} + photo.validate() + return photo + } + + // Convert to plain object for persistence + public toObject(): PhotoProps { + return { + id: this.id, + url: this.url, + userId: this.userId, + uploadedAt: this.createdAt, + metadata: this.metadata, + } + } +} diff --git a/src/modules/photo/entities/photo.entity.ts b/src/modules/photo/entities/photo.entity.ts index 589a3c3..67498e6 100644 --- a/src/modules/photo/entities/photo.entity.ts +++ b/src/modules/photo/entities/photo.entity.ts @@ -1,89 +1,2 @@ -// src/modules/photo/domain/entities/photo.entity.ts - -export interface PhotoProps { - id?: string; - url: string; - userId: string; - uploadedAt?: Date; - metadata?: Record; - } - - export class Photo { - private _id: string; - private _url: string; - private _userId: string; - private _uploadedAt: Date; - private _metadata?: Record; - - constructor(props: PhotoProps) { - this._id = props.id || crypto.randomUUID(); - this._url = props.url; - this._userId = props.userId; - this._uploadedAt = props.uploadedAt || new Date(); - this._metadata = props.metadata; - } - - // Getters - get id(): string { - return this._id; - } - - get url(): string { - return this._url; - } - - get userId(): string { - return this._userId; - } - - get uploadedAt(): Date { - return this._uploadedAt; - } - - get metadata(): Record | undefined { - return this._metadata; - } - - // Domain logic and validation - validate(): boolean { - if (!this._url || this._url.trim() === "") { - throw new Error("Photo URL is required"); - } - - if (!/^https?:\/\/.+$/.test(this._url)) { - throw new Error("Photo URL must be a valid HTTP/HTTPS URL"); - } - - if (!this._userId || this._userId.trim() === "") { - throw new Error("User ID is required"); - } - - return true; - } - - // Update metadata - updateMetadata(newMetadata: Record): void { - this._metadata = { - ...this._metadata, - ...newMetadata - }; - } - - // Static factory method - static create(props: PhotoProps): Photo { - const photo = new Photo(props); - photo.validate(); - return photo; - } - - // Convert to plain object for persistence - toObject(): PhotoProps { - return { - id: this._id, - url: this._url, - userId: this._userId, - uploadedAt: this._uploadedAt, - metadata: this._metadata, - }; - } - } \ No newline at end of file +// Re-export the domain entity +export { Photo, PhotoProps } from "../domain/entities/photo.entity" diff --git a/src/modules/project/__tests__/domain/entities/project.entity.test.ts b/src/modules/project/__tests__/domain/entities/project.entity.test.ts new file mode 100644 index 0000000..f926fed --- /dev/null +++ b/src/modules/project/__tests__/domain/entities/project.entity.test.ts @@ -0,0 +1,82 @@ +import { Project, ProjectStatus } from "../../../domain/entities/project.entity" + +describe("Project Entity", () => { + let project: Project + + beforeEach(() => { + project = new Project() + project.name = "Test Project" + project.description = "Test Description" + project.location = "Test Location" + project.startDate = new Date("2024-01-01") + project.endDate = new Date("2024-12-31") + project.organizationId = "org-123" + project.status = ProjectStatus.DRAFT + }) + + describe("Creation", () => { + it("should create a project with valid properties", () => { + expect(project.name).toBe("Test Project") + expect(project.description).toBe("Test Description") + expect(project.location).toBe("Test Location") + expect(project.status).toBe(ProjectStatus.DRAFT) + }) + + it("should have default status as DRAFT", () => { + const newProject = new Project() + expect(newProject.status).toBe(ProjectStatus.DRAFT) + }) + }) + + describe("Status Management", () => { + it("should activate a draft project", () => { + project.activate() + expect(project.status).toBe(ProjectStatus.ACTIVE) + }) + + it("should throw error when activating non-draft project", () => { + project.status = ProjectStatus.ACTIVE + expect(() => project.activate()).toThrow("Only draft projects can be activated") + }) + + it("should complete an active project", () => { + project.status = ProjectStatus.ACTIVE + project.complete() + expect(project.status).toBe(ProjectStatus.COMPLETED) + }) + + it("should throw error when completing non-active project", () => { + expect(() => project.complete()).toThrow("Only active projects can be completed") + }) + + it("should cancel a draft or active project", () => { + project.cancel() + expect(project.status).toBe(ProjectStatus.CANCELLED) + + project.status = ProjectStatus.ACTIVE + project.cancel() + expect(project.status).toBe(ProjectStatus.CANCELLED) + }) + + it("should throw error when cancelling completed project", () => { + project.status = ProjectStatus.COMPLETED + expect(() => project.cancel()).toThrow("Completed projects cannot be cancelled") + }) + }) + + describe("Status Checks", () => { + it("should check if project is active", () => { + expect(project.isActive()).toBe(false) + + project.status = ProjectStatus.ACTIVE + expect(project.isActive()).toBe(true) + }) + + it("should check if project is completed", () => { + expect(project.isCompleted()).toBe(false) + + project.status = ProjectStatus.COMPLETED + expect(project.isCompleted()).toBe(true) + }) + }) +}) diff --git a/src/modules/project/domain/Project.ts b/src/modules/project/domain/Project.ts index 180e563..bb837e4 100644 --- a/src/modules/project/domain/Project.ts +++ b/src/modules/project/domain/Project.ts @@ -1,60 +1,60 @@ -import { Entity } from '../../../entities/Entity'; +import { Entity } from "@/entities/Entity" export interface IProject { - id: string; - title: string; - description: string; - organizationId: string; - status: ProjectStatus; - createdAt: Date; - updatedAt: Date; + id: string + title: string + description: string + organizationId: string + status: ProjectStatus + createdAt: Date + updatedAt: Date } export enum ProjectStatus { - DRAFT = 'DRAFT', - ACTIVE = 'ACTIVE', - COMPLETED = 'COMPLETED', - CANCELLED = 'CANCELLED' + DRAFT = "DRAFT", + ACTIVE = "ACTIVE", + COMPLETED = "COMPLETED", + CANCELLED = "CANCELLED", } export class Project extends Entity { private constructor(props: IProject) { - super(props); + super(props) } - public static create(props: Omit): Project { + public static create(props: Omit): Project { return new Project({ ...props, id: crypto.randomUUID(), createdAt: new Date(), - updatedAt: new Date() - }); + updatedAt: new Date(), + }) } - public update(props: Partial>): void { + public update(props: Partial>): void { Object.assign(this.props, { ...props, - updatedAt: new Date() - }); + updatedAt: new Date(), + }) } public get id(): string { - return this.props.id; + return this.props.id } public get title(): string { - return this.props.title; + return this.props.title } public get description(): string { - return this.props.description; + return this.props.description } public get organizationId(): string { - return this.props.organizationId; + return this.props.organizationId } public get status(): ProjectStatus { - return this.props.status; + return this.props.status } } diff --git a/src/modules/project/domain/entities/project.entity.ts b/src/modules/project/domain/entities/project.entity.ts new file mode 100644 index 0000000..e523706 --- /dev/null +++ b/src/modules/project/domain/entities/project.entity.ts @@ -0,0 +1,74 @@ +import { Entity, Column, OneToMany } from "typeorm" +import { BaseEntity } from "../../../shared/domain/entities/base.entity" +import { Volunteer } from "@/modules/volunteer/domain/entities/volunteer.entity" + +export enum ProjectStatus { + DRAFT = "DRAFT", + ACTIVE = "ACTIVE", + COMPLETED = "COMPLETED", + CANCELLED = "CANCELLED", +} + +@Entity("projects") +export class Project extends BaseEntity { + @Column({ type: "varchar", length: 255, nullable: false }) + name: string + + @Column({ type: "text", nullable: false }) + description: string + + @Column({ type: "varchar", length: 255, nullable: false }) + location: string + + @Column({ type: "date", nullable: false }) + startDate: Date + + @Column({ type: "date", nullable: false }) + endDate: Date + + @Column({ type: "uuid", nullable: false }) + organizationId: string + + @Column({ + type: "enum", + enum: ProjectStatus, + default: ProjectStatus.DRAFT, + }) + status: ProjectStatus + + @OneToMany( + () => Volunteer, + (volunteer) => volunteer.project, + ) + volunteers?: Volunteer[] + + // Domain methods + public activate(): void { + if (this.status !== ProjectStatus.DRAFT) { + throw new Error("Only draft projects can be activated") + } + this.status = ProjectStatus.ACTIVE + } + + public complete(): void { + if (this.status !== ProjectStatus.ACTIVE) { + throw new Error("Only active projects can be completed") + } + this.status = ProjectStatus.COMPLETED + } + + public cancel(): void { + if (this.status === ProjectStatus.COMPLETED) { + throw new Error("Completed projects cannot be cancelled") + } + this.status = ProjectStatus.CANCELLED + } + + public isActive(): boolean { + return this.status === ProjectStatus.ACTIVE + } + + public isCompleted(): boolean { + return this.status === ProjectStatus.COMPLETED + } +} diff --git a/src/modules/shared/__tests__/domain/entities/test-item.entity.test.ts b/src/modules/shared/__tests__/domain/entities/test-item.entity.test.ts new file mode 100644 index 0000000..b790cb2 --- /dev/null +++ b/src/modules/shared/__tests__/domain/entities/test-item.entity.test.ts @@ -0,0 +1,90 @@ +import { TestItem } from "../../../domain/entities/test-item.entity" + +describe("TestItem Entity", () => { + describe("Creation", () => { + it("should create a test item with valid properties", () => { + const testItem = TestItem.create("Test Item", 100, 5) + + expect(testItem).toBeInstanceOf(TestItem) + expect(testItem.name).toBe("Test Item") + expect(testItem.value).toBe(100) + expect(testItem.age).toBe(5) + }) + + it("should throw error if name is empty", () => { + expect(() => { + TestItem.create("", 100, 5) + }).toThrow("Name is required") + }) + + it("should throw error if name is only whitespace", () => { + expect(() => { + TestItem.create(" ", 100, 5) + }).toThrow("Name is required") + }) + + it("should throw error if value is negative", () => { + expect(() => { + TestItem.create("Test Item", -1, 5) + }).toThrow("Value must be non-negative") + }) + + it("should throw error if age is negative", () => { + expect(() => { + TestItem.create("Test Item", 100, -1) + }).toThrow("Age must be non-negative") + }) + + it("should allow zero values", () => { + expect(() => { + TestItem.create("Test Item", 0, 0) + }).not.toThrow() + }) + }) + + describe("Value Management", () => { + let testItem: TestItem + + beforeEach(() => { + testItem = TestItem.create("Test Item", 100, 5) + }) + + it("should update value with valid number", () => { + testItem.updateValue(200) + expect(testItem.value).toBe(200) + }) + + it("should allow updating value to zero", () => { + testItem.updateValue(0) + expect(testItem.value).toBe(0) + }) + + it("should throw error when updating value to negative", () => { + expect(() => { + testItem.updateValue(-10) + }).toThrow("Value must be non-negative") + }) + }) + + describe("Age Management", () => { + let testItem: TestItem + + beforeEach(() => { + testItem = TestItem.create("Test Item", 100, 5) + }) + + it("should increment age by 1", () => { + const originalAge = testItem.age + testItem.incrementAge() + expect(testItem.age).toBe(originalAge + 1) + }) + + it("should increment age multiple times", () => { + const originalAge = testItem.age + testItem.incrementAge() + testItem.incrementAge() + testItem.incrementAge() + expect(testItem.age).toBe(originalAge + 3) + }) + }) +}) diff --git a/src/modules/shared/domain/entities/base.entity.ts b/src/modules/shared/domain/entities/base.entity.ts index 65f88af..118c785 100644 --- a/src/modules/shared/domain/entities/base.entity.ts +++ b/src/modules/shared/domain/entities/base.entity.ts @@ -1,11 +1,32 @@ +import { PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" + export abstract class BaseEntity { - public readonly id?: string; - public readonly createdAt?: Date; - public readonly updatedAt?: Date; - - constructor(id?: string, createdAt?: Date, updatedAt?: Date) { - this.id = id; - this.createdAt = createdAt; - this.updatedAt = updatedAt; + @PrimaryGeneratedColumn("uuid") + id: string + + @CreateDateColumn() + createdAt: Date + + @UpdateDateColumn() + updatedAt: Date +} + +export abstract class Entity { + protected readonly props: T + + constructor(props: T) { + this.props = props + } + + public equals(entity: Entity): boolean { + if (entity === null || entity === undefined) { + return false + } + + if (this === entity) { + return true + } + + return JSON.stringify(this.props) === JSON.stringify(entity.props) } } diff --git a/src/modules/shared/domain/entities/test-item.entity.ts b/src/modules/shared/domain/entities/test-item.entity.ts new file mode 100644 index 0000000..acbe2b5 --- /dev/null +++ b/src/modules/shared/domain/entities/test-item.entity.ts @@ -0,0 +1,47 @@ +import { Entity, Column } from "typeorm" +import { BaseEntity } from "./base.entity" + +@Entity("test_items") +export class TestItem extends BaseEntity { + @Column() + name: string + + @Column() + value: number + + @Column() + age: number + + // Domain methods + public static create(name: string, value: number, age: number): TestItem { + if (!name || name.trim() === "") { + throw new Error("Name is required") + } + + if (value < 0) { + throw new Error("Value must be non-negative") + } + + if (age < 0) { + throw new Error("Age must be non-negative") + } + + const testItem = new TestItem() + testItem.name = name + testItem.value = value + testItem.age = age + + return testItem + } + + public updateValue(newValue: number): void { + if (newValue < 0) { + throw new Error("Value must be non-negative") + } + this.value = newValue + } + + public incrementAge(): void { + this.age += 1 + } +} diff --git a/src/modules/user/__tests__/domain/entities/user-volunteer.entity.test.ts b/src/modules/user/__tests__/domain/entities/user-volunteer.entity.test.ts new file mode 100644 index 0000000..2fc5b4e --- /dev/null +++ b/src/modules/user/__tests__/domain/entities/user-volunteer.entity.test.ts @@ -0,0 +1,64 @@ +import { UserVolunteer } from "../../../domain/entities/user-volunteer.entity" + +describe("UserVolunteer Entity", () => { + const validUserId = "user-123" + const validVolunteerId = "volunteer-456" + + describe("Creation", () => { + it("should create a user-volunteer association with valid IDs", () => { + const userVolunteer = UserVolunteer.create(validUserId, validVolunteerId) + + expect(userVolunteer).toBeInstanceOf(UserVolunteer) + expect(userVolunteer.userId).toBe(validUserId) + expect(userVolunteer.volunteerId).toBe(validVolunteerId) + expect(userVolunteer.joinedAt).toBeInstanceOf(Date) + }) + + it("should throw error if userId is empty", () => { + expect(() => { + UserVolunteer.create("", validVolunteerId) + }).toThrow("User ID and Volunteer ID are required") + }) + + it("should throw error if volunteerId is empty", () => { + expect(() => { + UserVolunteer.create(validUserId, "") + }).toThrow("User ID and Volunteer ID are required") + }) + + it("should throw error if both IDs are empty", () => { + expect(() => { + UserVolunteer.create("", "") + }).toThrow("User ID and Volunteer ID are required") + }) + }) + + describe("Assignment Checks", () => { + let userVolunteer: UserVolunteer + + beforeEach(() => { + userVolunteer = UserVolunteer.create(validUserId, validVolunteerId) + }) + + it("should check if user is assigned", () => { + expect(userVolunteer.isUserAssigned(validUserId)).toBe(true) + expect(userVolunteer.isUserAssigned("other-user")).toBe(false) + }) + + it("should check if volunteer is assigned", () => { + expect(userVolunteer.isVolunteerAssigned(validVolunteerId)).toBe(true) + expect(userVolunteer.isVolunteerAssigned("other-volunteer")).toBe(false) + }) + }) + + describe("Timestamps", () => { + it("should set joinedAt timestamp on creation", () => { + const beforeCreation = new Date() + const userVolunteer = UserVolunteer.create(validUserId, validVolunteerId) + const afterCreation = new Date() + + expect(userVolunteer.joinedAt.getTime()).toBeGreaterThanOrEqual(beforeCreation.getTime()) + expect(userVolunteer.joinedAt.getTime()).toBeLessThanOrEqual(afterCreation.getTime()) + }) + }) +}) diff --git a/src/modules/user/__tests__/domain/entities/user.entity.test.ts b/src/modules/user/__tests__/domain/entities/user.entity.test.ts new file mode 100644 index 0000000..6737848 --- /dev/null +++ b/src/modules/user/__tests__/domain/entities/user.entity.test.ts @@ -0,0 +1,60 @@ +import { User } from "../../../domain/entities/user.entity" + +describe("User Entity", () => { + describe("Creation", () => { + it("should create a user with valid properties", () => { + const user = new User() + user.name = "John" + user.lastName = "Doe" + user.email = "john.doe@example.com" + user.password = "hashedPassword" + user.wallet = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + user.isVerified = false + + expect(user.name).toBe("John") + expect(user.lastName).toBe("Doe") + expect(user.email).toBe("john.doe@example.com") + expect(user.isVerified).toBe(false) + }) + + it("should have default verification status as false", () => { + const user = new User() + expect(user.isVerified).toBe(false) + }) + }) + + describe("Email Verification", () => { + it("should set verification token and expiry", () => { + const user = new User() + const token = "verification-token-123" + const expiry = new Date(Date.now() + 3600000) // 1 hour from now + + user.verificationToken = token + user.verificationTokenExpires = expiry + + expect(user.verificationToken).toBe(token) + expect(user.verificationTokenExpires).toBe(expiry) + }) + + it("should mark user as verified", () => { + const user = new User() + user.isVerified = true + user.verificationToken = "" + + expect(user.isVerified).toBe(true) + expect(user.verificationToken).toBe("") + expect(user.verificationTokenExpires).toBeNull() + }) + }) + + describe("Wallet Integration", () => { + it("should store wallet address", () => { + const user = new User() + const walletAddress = "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + + user.wallet = walletAddress + + expect(user.wallet).toBe(walletAddress) + }) + }) +}) diff --git a/src/modules/user/__tests__/user.test.ts b/src/modules/user/__tests__/user.test.ts index 9f5d574..110e0bb 100644 --- a/src/modules/user/__tests__/user.test.ts +++ b/src/modules/user/__tests__/user.test.ts @@ -1,17 +1,17 @@ -import { PrismaClient } from "@prisma/client"; +import { PrismaClient } from "@prisma/client" -const prisma = new PrismaClient(); +const prisma = new PrismaClient() describe("Prisma User Operations", () => { - let userId: number; + let userId: number beforeAll(async () => { - await prisma.users.deleteMany(); - }); + await prisma.users.deleteMany() + }) afterAll(async () => { - await prisma.$disconnect(); - }); + await prisma.$disconnect() + }) test("Should create a new user", async () => { const newUser = await prisma.users.create({ @@ -21,42 +21,42 @@ describe("Prisma User Operations", () => { password_hash: "hashed_password", role: "user", }, - }); + }) - userId = newUser.id; // Store user ID for further tests - expect(newUser).toHaveProperty("id"); - expect(newUser.email).toBe("john@example.com"); - }); + userId = newUser.id // Store user ID for further tests + expect(newUser).toHaveProperty("id") + expect(newUser.email).toBe("john@example.com") + }) test("Should retrieve all users", async () => { - const allUsers = await prisma.users.findMany(); - expect(Array.isArray(allUsers)).toBe(true); - expect(allUsers.length).toBeGreaterThan(0); - }); + const allUsers = await prisma.users.findMany() + expect(Array.isArray(allUsers)).toBe(true) + expect(allUsers.length).toBeGreaterThan(0) + }) test("Should find user by email", async () => { const user = await prisma.users.findUnique({ where: { email: "john@example.com" }, - }); + }) - expect(user).not.toBeNull(); - expect(user?.email).toBe("john@example.com"); - }); + expect(user).not.toBeNull() + expect(user?.email).toBe("john@example.com") + }) test("Should update user email", async () => { const updatedUser = await prisma.users.update({ where: { id: userId }, data: { email: "john_updated@example.com" }, - }); + }) - expect(updatedUser.email).toBe("john_updated@example.com"); - }); + expect(updatedUser.email).toBe("john_updated@example.com") + }) test("Should delete a user", async () => { const deletedUser = await prisma.users.delete({ where: { id: userId }, - }); + }) - expect(deletedUser.id).toBe(userId); - }); -}); + expect(deletedUser.id).toBe(userId) + }) +}) diff --git a/src/modules/user/domain/entities/User.entity.ts b/src/modules/user/domain/entities/User.entity.ts new file mode 100644 index 0000000..1a05511 --- /dev/null +++ b/src/modules/user/domain/entities/User.entity.ts @@ -0,0 +1,31 @@ +import { Entity, Column, BaseEntity, PrimaryGeneratedColumn } from "typeorm" + +@Entity("users") +export class User extends BaseEntity { + @PrimaryGeneratedColumn("uuid") + id: string + + @Column() + name: string + + @Column() + lastName: string + + @Column({ unique: true }) + email: string + + @Column() + password: string + + @Column({ unique: true }) + wallet: string + + @Column({ default: false }) + isVerified: boolean + + @Column({ nullable: true }) + verificationToken: string + + @Column({ type: "timestamp", nullable: true }) + verificationTokenExpires: Date +} diff --git a/src/modules/user/domain/entities/User.ts b/src/modules/user/domain/entities/User.ts index 76221b3..6956cb9 100644 --- a/src/modules/user/domain/entities/User.ts +++ b/src/modules/user/domain/entities/User.ts @@ -1,31 +1,2 @@ -import { Entity, Column, BaseEntity, PrimaryGeneratedColumn } from "typeorm"; - -@Entity() -export class User extends BaseEntity { - @PrimaryGeneratedColumn("uuid") - id: string; - - @Column() - name: string; - - @Column() - lastName: string; - - @Column({ unique: true }) - email: string; - - @Column() - password: string; - - @Column({ unique: true }) - wallet: string; - - @Column({ default: false }) - isVerified: boolean; - - @Column({ nullable: true }) - verificationToken: string; - - @Column({ nullable: true }) - verificationTokenExpires: Date; -} \ No newline at end of file +// Re-export the main user entity +export { User } from "./User.entity" diff --git a/src/modules/user/domain/entities/user-volunteer.entity.ts b/src/modules/user/domain/entities/user-volunteer.entity.ts new file mode 100644 index 0000000..b635077 --- /dev/null +++ b/src/modules/user/domain/entities/user-volunteer.entity.ts @@ -0,0 +1,36 @@ +import { Entity, PrimaryColumn, CreateDateColumn } from "typeorm" +import { BaseEntity } from "../../../shared/domain/entities/base.entity" + +@Entity("user_volunteers") +export class UserVolunteer extends BaseEntity { + @PrimaryColumn("uuid") + userId: string + + @PrimaryColumn("uuid") + volunteerId: string + + @CreateDateColumn() + joinedAt: Date + + // Domain methods + public static create(userId: string, volunteerId: string): UserVolunteer { + if (!userId || !volunteerId) { + throw new Error("User ID and Volunteer ID are required") + } + + const userVolunteer = new UserVolunteer() + userVolunteer.userId = userId + userVolunteer.volunteerId = volunteerId + userVolunteer.joinedAt = new Date() + + return userVolunteer + } + + public isUserAssigned(userId: string): boolean { + return this.userId === userId + } + + public isVolunteerAssigned(volunteerId: string): boolean { + return this.volunteerId === volunteerId + } +} diff --git a/src/modules/user/use-cases/userUseCase.ts b/src/modules/user/use-cases/userUseCase.ts index c6a2aa7..fe4e8a0 100644 --- a/src/modules/user/use-cases/userUseCase.ts +++ b/src/modules/user/use-cases/userUseCase.ts @@ -1,6 +1,6 @@ import { IUserRepository } from "../domain/interfaces/IUserRepository"; import { CreateUserDto } from "../dto/CreateUserDto"; -import { User } from "../domain/entities/User"; +import { User } from "../domain/entities/User.entity"; import bcrypt from "bcryptjs"; import { UpdateUserDto } from "../dto/UpdateUserDto"; diff --git a/src/modules/volunteer/.DS_Store b/src/modules/volunteer/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/src/modules/volunteer/.DS_Store and /dev/null differ diff --git a/src/modules/volunteer/__tests__/domain/volunteer.entity.test.ts b/src/modules/volunteer/__tests__/domain/volunteer.entity.test.ts index a29e2c1..18c4c8b 100644 --- a/src/modules/volunteer/__tests__/domain/volunteer.entity.test.ts +++ b/src/modules/volunteer/__tests__/domain/volunteer.entity.test.ts @@ -1,4 +1,4 @@ -import { Volunteer } from "../../domain/volunteer.entity"; +import { Volunteer } from "../../../volunteer/domain/entities/volunteer.entity" describe("Volunteer Entity", () => { const validVolunteerProps = { @@ -7,27 +7,27 @@ describe("Volunteer Entity", () => { requirements: "Test Requirements", projectId: "project-123", incentive: "Test Incentive", - }; + } describe("Creation", () => { it("should create a volunteer with valid props", () => { - const volunteer = Volunteer.create(validVolunteerProps); + const volunteer = Volunteer.create(validVolunteerProps) - expect(volunteer).toBeInstanceOf(Volunteer); - expect(volunteer.name).toBe(validVolunteerProps.name); - expect(volunteer.description).toBe(validVolunteerProps.description); - expect(volunteer.requirements).toBe(validVolunteerProps.requirements); - expect(volunteer.projectId).toBe(validVolunteerProps.projectId); - expect(volunteer.incentive).toBe(validVolunteerProps.incentive); - }); + expect(volunteer).toBeInstanceOf(Volunteer) + expect(volunteer.name).toBe(validVolunteerProps.name) + expect(volunteer.description).toBe(validVolunteerProps.description) + expect(volunteer.requirements).toBe(validVolunteerProps.requirements) + expect(volunteer.projectId).toBe(validVolunteerProps.projectId) + expect(volunteer.incentive).toBe(validVolunteerProps.incentive) + }) it("should generate a UUID if not provided", () => { - const volunteer = Volunteer.create(validVolunteerProps); + const volunteer = Volunteer.create(validVolunteerProps) - expect(volunteer.id).toBeTruthy(); - expect(volunteer.id.length).toBeGreaterThan(0); - }); - }); + expect(volunteer.id).toBeTruthy() + expect(volunteer.id.length).toBeGreaterThan(0) + }) + }) describe("Validation", () => { it("should throw error if name is empty", () => { @@ -35,69 +35,77 @@ describe("Volunteer Entity", () => { Volunteer.create({ ...validVolunteerProps, name: "", - }); - }).toThrow("Name is required"); - }); + }) + }).toThrow("Name is required") + }) it("should throw error if description is empty", () => { expect(() => { Volunteer.create({ ...validVolunteerProps, description: "", - }); - }).toThrow("Description is required"); - }); + }) + }).toThrow("Description is required") + }) it("should throw error if requirements is empty", () => { expect(() => { Volunteer.create({ ...validVolunteerProps, requirements: "", - }); - }).toThrow("Requirements are required"); - }); + }) + }).toThrow("Requirements are required") + }) it("should throw error if projectId is missing", () => { expect(() => { Volunteer.create({ ...validVolunteerProps, projectId: "", - }); - }).toThrow("Project ID is required"); - }); - }); + }) + }).toThrow("Project ID is required") + }) + }) describe("Update", () => { it("should update volunteer properties", () => { - const volunteer = Volunteer.create(validVolunteerProps); + const volunteer = Volunteer.create(validVolunteerProps) volunteer.update({ name: "Updated Name", description: "Updated Description", requirements: "Updated Requirements", incentive: "Updated Incentive", - }); + }) - expect(volunteer.name).toBe("Updated Name"); - expect(volunteer.description).toBe("Updated Description"); - expect(volunteer.requirements).toBe("Updated Requirements"); - expect(volunteer.incentive).toBe("Updated Incentive"); - }); + expect(volunteer.name).toBe("Updated Name") + expect(volunteer.description).toBe("Updated Description") + expect(volunteer.requirements).toBe("Updated Requirements") + expect(volunteer.incentive).toBe("Updated Incentive") + }) it("should not modify properties if not provided", () => { - const volunteer = Volunteer.create(validVolunteerProps); - const originalName = volunteer.name; + const volunteer = Volunteer.create(validVolunteerProps) + const originalName = volunteer.name - volunteer.update({}); + volunteer.update({}) - expect(volunteer.name).toBe(originalName); - }); - }); + expect(volunteer.name).toBe(originalName) + }) + + it("should throw error when updating with invalid data", () => { + const volunteer = Volunteer.create(validVolunteerProps) + + expect(() => volunteer.update({ name: "" })).toThrow("Name is required") + expect(() => volunteer.update({ description: "" })).toThrow("Description is required") + expect(() => volunteer.update({ requirements: "" })).toThrow("Requirements are required") + }) + }) describe("ToObject", () => { it("should convert entity to plain object", () => { - const volunteer = Volunteer.create(validVolunteerProps); - const volunteerObject = volunteer.toObject(); + const volunteer = Volunteer.create(validVolunteerProps) + const volunteerObject = volunteer.toObject() expect(volunteerObject).toEqual( expect.objectContaining({ @@ -106,8 +114,8 @@ describe("Volunteer Entity", () => { requirements: validVolunteerProps.requirements, projectId: validVolunteerProps.projectId, incentive: validVolunteerProps.incentive, - }) - ); - }); - }); -}); + }), + ) + }) + }) +}) diff --git a/src/modules/volunteer/domain/entities/volunteer.entity.ts b/src/modules/volunteer/domain/entities/volunteer.entity.ts new file mode 100644 index 0000000..cbabf76 --- /dev/null +++ b/src/modules/volunteer/domain/entities/volunteer.entity.ts @@ -0,0 +1,111 @@ +import { Entity, Column, JoinColumn, ManyToOne } from "typeorm" +import { BaseEntity } from "../../../shared/domain/entities/base.entity" +import { Project } from "../../../project/domain/entities/project.entity" + +export interface VolunteerProps { + name: string + description: string + requirements: string + projectId: string + incentive?: string +} + +@Entity("volunteers") +export class Volunteer extends BaseEntity { + @Column({ type: "varchar", length: 255, nullable: false }) + name!: string + + @Column({ type: "varchar", length: 255, nullable: false }) + description!: string + + @Column({ type: "varchar", length: 255, nullable: false }) + requirements!: string + + @Column({ nullable: true }) + incentive?: string + + @Column({ type: "uuid", nullable: false }) + projectId!: string + + @ManyToOne( + () => Project, + (project) => project.volunteers, + { + nullable: false, + }, + ) + @JoinColumn({ name: "projectId" }) + project!: Project + + // Domain methods + public static create(props: VolunteerProps): Volunteer { + const volunteer = new Volunteer() + volunteer.validateProps(props) + + volunteer.name = props.name + volunteer.description = props.description + volunteer.requirements = props.requirements + volunteer.projectId = props.projectId + volunteer.incentive = props.incentive + + return volunteer + } + + public update(props: Partial): void { + if (props.name !== undefined) { + if (!props.name.trim()) { + throw new Error("Name is required") + } + this.name = props.name + } + + if (props.description !== undefined) { + if (!props.description.trim()) { + throw new Error("Description is required") + } + this.description = props.description + } + + if (props.requirements !== undefined) { + if (!props.requirements.trim()) { + throw new Error("Requirements are required") + } + this.requirements = props.requirements + } + + if (props.incentive !== undefined) { + this.incentive = props.incentive + } + } + + public toObject(): VolunteerProps & { id: string; createdAt: Date; updatedAt: Date } { + return { + id: this.id, + name: this.name, + description: this.description, + requirements: this.requirements, + projectId: this.projectId, + incentive: this.incentive, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + } + } + + private validateProps(props: VolunteerProps): void { + if (!props.name || !props.name.trim()) { + throw new Error("Name is required") + } + + if (!props.description || !props.description.trim()) { + throw new Error("Description is required") + } + + if (!props.requirements || !props.requirements.trim()) { + throw new Error("Requirements are required") + } + + if (!props.projectId || !props.projectId.trim()) { + throw new Error("Project ID is required") + } + } +} diff --git a/src/modules/volunteer/domain/volunteer.entity.ts b/src/modules/volunteer/domain/volunteer.entity.ts deleted file mode 100644 index 9bdf0c0..0000000 --- a/src/modules/volunteer/domain/volunteer.entity.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { IVolunteerProps, IVolunteerUpdateProps } from "./volunteer.interface"; - -export class Volunteer { - private _id: string; - private _name: string; - private _description: string; - private _requirements: string; - private _incentive?: string | null; - private _projectId: string; - private _createdAt: Date; - private _updatedAt: Date; - - constructor(props: IVolunteerProps) { - this._id = props.id || crypto.randomUUID(); - this._name = props.name; - this._description = props.description; - this._requirements = props.requirements; - this._incentive = props.incentive; - this._projectId = props.projectId; - this._createdAt = props.createdAt || new Date(); - this._updatedAt = props.updatedAt || new Date(); - } - - // Getters - get id(): string { - return this._id; - } - get name(): string { - return this._name; - } - get description(): string { - return this._description; - } - get requirements(): string { - return this._requirements; - } - get incentive(): string | null | undefined { - return this._incentive; - } - get projectId(): string { - return this._projectId; - } - get createdAt(): Date { - return this._createdAt; - } - get updatedAt(): Date { - return this._updatedAt; - } - - // Update method for domain logic - // update(props: IVolunteerUpdateProps): void { - // if (props.name) this._name = props.name; - // if (props.description) this._description = props.description; - // if (props.requirements) this._requirements = props.requirements; - // if (props.incentive !== undefined) this._incentive = props.incentive; - - // this._updatedAt = new Date(); - // } - - update(props: IVolunteerUpdateProps): void { - if (props.name !== undefined) { - if (!props.name.trim()) throw new Error("Name is required"); - this._name = props.name; - } - - if (props.description !== undefined) { - if (!props.description.trim()) throw new Error("Description is required"); - this._description = props.description; - } - - if (props.requirements !== undefined) { - if (!props.requirements.trim()) - throw new Error("Requirements are required"); - this._requirements = props.requirements; - } - - if (props.incentive !== undefined) { - this._incentive = props.incentive; - } - - this._updatedAt = new Date(); - } - - // Validation method - validate(): boolean { - if (!this._name || this._name.trim() === "") { - throw new Error("Name is required"); - } - if (!this._description || this._description.trim() === "") { - throw new Error("Description is required"); - } - if (!this._requirements || this._requirements.trim() === "") { - throw new Error("Requirements are required"); - } - if (!this._projectId) { - throw new Error("Project ID is required"); - } - return true; - } - - // Static method to create a new Volunteer - static create(props: IVolunteerProps): Volunteer { - const volunteer = new Volunteer(props); - volunteer.validate(); - return volunteer; - } - - // Converts entity to plain object for persistence - toObject(): IVolunteerProps { - return { - id: this._id, - name: this._name, - description: this._description, - requirements: this._requirements, - incentive: this._incentive, - projectId: this._projectId, - createdAt: this._createdAt, - updatedAt: this._updatedAt, - }; - } -} diff --git a/src/modules/volunteer/repositories/implementations/volunteer-prisma.repository.ts b/src/modules/volunteer/repositories/implementations/volunteer-prisma.repository.ts index 948e159..e790727 100644 --- a/src/modules/volunteer/repositories/implementations/volunteer-prisma.repository.ts +++ b/src/modules/volunteer/repositories/implementations/volunteer-prisma.repository.ts @@ -1,5 +1,5 @@ import { PrismaClient } from "@prisma/client"; -import { Volunteer } from "../../domain/volunteer.entity"; +import { Volunteer } from "../../domain/entities/volunteer.entity"; import { IVolunteerRepository } from "../interfaces/volunteer-repository.interface"; export class VolunteerPrismaRepository implements IVolunteerRepository { diff --git a/src/modules/volunteer/repositories/interfaces/volunteer-repository.interface.ts b/src/modules/volunteer/repositories/interfaces/volunteer-repository.interface.ts index c1d28a7..42346f3 100644 --- a/src/modules/volunteer/repositories/interfaces/volunteer-repository.interface.ts +++ b/src/modules/volunteer/repositories/interfaces/volunteer-repository.interface.ts @@ -1,4 +1,4 @@ -import { Volunteer } from "../../domain/volunteer.entity"; +import { Volunteer } from "../../domain/entities/volunteer.entity"; export interface IVolunteerRepository { create(volunteer: Volunteer): Promise; diff --git a/src/modules/volunteer/use-cases/create-volunteer.use-case.ts b/src/modules/volunteer/use-cases/create-volunteer.use-case.ts index d0eadd0..d1a19cb 100644 --- a/src/modules/volunteer/use-cases/create-volunteer.use-case.ts +++ b/src/modules/volunteer/use-cases/create-volunteer.use-case.ts @@ -1,4 +1,4 @@ -import { Volunteer } from "../domain/volunteer.entity"; +import { Volunteer } from "../domain/entities/volunteer.entity"; import { IVolunteerRepository } from "../repositories/interfaces/volunteer-repository.interface"; import { CreateVolunteerDTO } from "../dto/volunteer.dto"; diff --git a/src/modules/volunteer/use-cases/get-volunteers-by-project.use-case.ts b/src/modules/volunteer/use-cases/get-volunteers-by-project.use-case.ts index 7177d0e..cd80f9e 100644 --- a/src/modules/volunteer/use-cases/get-volunteers-by-project.use-case.ts +++ b/src/modules/volunteer/use-cases/get-volunteers-by-project.use-case.ts @@ -1,4 +1,4 @@ -import { Volunteer } from "../domain/volunteer.entity"; +import { Volunteer } from "../domain/entities/volunteer.entity"; import { IVolunteerRepository } from "../repositories/interfaces/volunteer-repository.interface"; export class GetVolunteersByProjectUseCase { diff --git a/src/modules/volunteer/use-cases/update-volunteer.use-case.ts b/src/modules/volunteer/use-cases/update-volunteer.use-case.ts index 12166b8..c29e6fd 100644 --- a/src/modules/volunteer/use-cases/update-volunteer.use-case.ts +++ b/src/modules/volunteer/use-cases/update-volunteer.use-case.ts @@ -1,4 +1,4 @@ -import { Volunteer } from "../domain/volunteer.entity"; +import { Volunteer } from "../domain/entities/volunteer.entity"; import { IVolunteerRepository } from "../repositories/interfaces/volunteer-repository.interface"; import { UpdateVolunteerDTO } from "../dto/volunteer.dto"; diff --git a/src/services/VolunteerService.ts b/src/services/VolunteerService.ts index aeb5e24..56daade 100644 --- a/src/services/VolunteerService.ts +++ b/src/services/VolunteerService.ts @@ -1,4 +1,4 @@ -import { Volunteer } from "../modules/volunteer/domain/volunteer.entity"; +import { Volunteer } from "../modules/volunteer/domain/entities/volunteer.entity"; import { VolunteerPrismaRepository } from "../modules/volunteer/repositories/implementations/volunteer-prisma.repository"; import { CreateVolunteerUseCase } from "../modules/volunteer/use-cases/create-volunteer.use-case"; import { GetVolunteersByProjectUseCase } from "../modules/volunteer/use-cases/get-volunteers-by-project.use-case";