Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/application/usecase/member/RegisterMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Email,
Member,
type MemberRepository,
StudentId,
UniversityEmail,
} from "#domain";

Expand Down Expand Up @@ -48,7 +49,7 @@ export class RegisterMemberUseCase extends IUseCase<
const member = new Member(
uuid(),
input.name,
input.studentId,
StudentId.fromString(input.studentId),
Department.fromString(input.department),
universityEmail,
personalEmail,
Expand Down
3 changes: 2 additions & 1 deletion src/application/usecase/member/UpdateMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Email,
type Member,
type MemberRepository,
StudentId,
UniversityEmail,
} from "#domain";

Expand Down Expand Up @@ -42,7 +43,7 @@ export class UpdateMemberUseCase extends IUseCase<
member.setName(input.name);
}
if (input.studentId) {
member.setStudentId(input.studentId);
member.setStudentId(StudentId.fromString(input.studentId));
}
if (input.department) {
member.setDepartment(Department.fromString(input.department));
Expand Down
7 changes: 4 additions & 3 deletions src/domain/aggregates/member/Member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
DiscordAccountAlreadyConnectedException,
DiscordAccountNotConnectedException,
} from "#domain/exceptions";
import type { StudentId } from "#domain/shared/StudentId";
import type { Department } from "./Departments";
import type { DiscordAccount } from "./DiscordAccount";
import type { Email } from "./Email";
Expand All @@ -13,7 +14,7 @@ export class Member {
constructor(
public readonly id: string,
private name: string,
private studentId: string,
private studentId: StudentId,
private department: Department,
private email: UniversityEmail,
private personalEmail?: Email,
Expand Down Expand Up @@ -47,7 +48,7 @@ export class Member {
this.name = newName;
}

setStudentId(newStudentId: string) {
setStudentId(newStudentId: StudentId) {
this.studentId = newStudentId;
}

Expand Down Expand Up @@ -99,7 +100,7 @@ export class Member {
return {
id: this.id,
name: this.name,
studentId: this.studentId,
studentId: this.studentId.getValue(),
department: this.department,
email: this.email,
personalEmail: this.personalEmail,
Expand Down
9 changes: 9 additions & 0 deletions src/domain/exceptions/DomainExceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,12 @@ export class ExhibitHasMemberException extends DomainException {
this.name = "ExhibitHasMemberException";
}
}

export class InvalidStudentIdException extends DomainException {
constructor(studentId: string) {
super(
`無効な学籍番号です: ${studentId} (8桁数字 または 3桁数字+英大文字1文字+4桁数字)`,
);
this.name = "InvalidStudentIdException";
}
}
1 change: 1 addition & 0 deletions src/domain/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./base";
export * from "./exceptions";
export * from "./aggregates";
export * from "./shared";
34 changes: 34 additions & 0 deletions src/domain/shared/StudentId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ValueObject } from "#domain/base/ValueObject";
import { InvalidStudentIdException } from "#domain/exceptions";

/**
* 学籍番号
*
* 2つのフォーマットに対応する:
* - 数字8桁(例: 70312031)
* - 数字3桁 + アルファベット1文字 + 数字4桁(例: 725A1061)
*
* 2025年度入学者から英字を含む形式に変更された。
* アルファベットは大文字に正規化して保持する。
*/
export class StudentId extends ValueObject<string> {
private static readonly NUMERIC_ONLY = /^[0-9]{8}$/;
private static readonly ALPHANUMERIC = /^[0-9]{3}[A-Z][0-9]{4}$/;

private constructor(value: string) {
super(value);
}
Comment thread
KinjiKawaguchi marked this conversation as resolved.

static fromString(value: string): StudentId {
return new StudentId(value.trim().toUpperCase());
}
Comment thread
KinjiKawaguchi marked this conversation as resolved.

protected validate(): void {
const isValid =
StudentId.NUMERIC_ONLY.test(this.value) ||
StudentId.ALPHANUMERIC.test(this.value);
if (!isValid) {
throw new InvalidStudentIdException(this.value);
}
}
Comment thread
KinjiKawaguchi marked this conversation as resolved.
}
1 change: 1 addition & 0 deletions src/domain/shared/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./StudentId";
3 changes: 2 additions & 1 deletion src/infrastructure/drizzle/DrizzleMemberRepository.ts
Comment thread
KinjiKawaguchi marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Email,
Member,
type MemberRepository,
StudentId,
UniversityEmail,
} from "#domain";
import { getDb } from "./client";
Expand All @@ -27,7 +28,7 @@ export class DrizzleMemberRepository implements MemberRepository {
const member = new Member(
record.id,
record.name,
record.studentId,
StudentId.fromString(record.studentId),
Comment thread
KinjiKawaguchi marked this conversation as resolved.
Department.fromString(record.department),
new UniversityEmail(record.email),
record.personalEmail ? new Email(record.personalEmail) : undefined,
Expand Down
77 changes: 77 additions & 0 deletions tests/domain/shared/StudentId.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import { InvalidStudentIdException } from "#domain/exceptions";
import { StudentId } from "#domain/shared/StudentId";

describe("StudentId", () => {
describe("fromString", () => {
it("旧形式(8桁数字)の学籍番号を生成できる", () => {
const id = StudentId.fromString("70312031");
expect(id.getValue()).toBe("70312031");
});

it("新形式(3桁数字+英字+4桁数字)の学籍番号を生成できる", () => {
const id = StudentId.fromString("725A1061");
expect(id.getValue()).toBe("725A1061");
});

it("小文字の英字を大文字に正規化する", () => {
const id = StudentId.fromString("725a1061");
expect(id.getValue()).toBe("725A1061");
});

it("前後の空白をトリムする", () => {
const id = StudentId.fromString(" 70312031 ");
expect(id.getValue()).toBe("70312031");
});
});

describe("バリデーション", () => {
it("7桁の数字は無効", () => {
expect(() => StudentId.fromString("7031203")).toThrow(
InvalidStudentIdException,
);
});

it("9桁の数字は無効", () => {
expect(() => StudentId.fromString("703120310")).toThrow(
InvalidStudentIdException,
);
});

it("空文字は無効", () => {
expect(() => StudentId.fromString("")).toThrow(InvalidStudentIdException);
});

it("英字のみは無効", () => {
expect(() => StudentId.fromString("ABCDEFGH")).toThrow(
InvalidStudentIdException,
);
});

it("新形式で英字が2文字あると無効", () => {
expect(() => StudentId.fromString("72AB1061")).toThrow(
InvalidStudentIdException,
);
});

it("新形式で英字の位置が異なると無効", () => {
expect(() => StudentId.fromString("7A251061")).toThrow(
InvalidStudentIdException,
);
});
});

describe("equals", () => {
it("同じ学籍番号は等しい", () => {
const a = StudentId.fromString("725A1061");
const b = StudentId.fromString("725a1061");
expect(a.equals(b)).toBe(true);
});

it("異なる学籍番号は等しくない", () => {
const a = StudentId.fromString("725A1061");
const b = StudentId.fromString("70312031");
expect(a.equals(b)).toBe(false);
});
});
});