Skip to content

Commit 57d625e

Browse files
ImTotemclaude
andcommitted
feat(api): add migration system, member fields, and refactor infrastructure
Introduce versioned migration system (V001 initial schema, V002 add department/student_id) with shared helpers. Refactor dependency caching to lru_cache, extract KST timezone module, coerce student_id from Sheets int to str via Pydantic field_validator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a552aff commit 57d625e

19 files changed

Lines changed: 261 additions & 145 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ n8n (automation workflows) → Google Sheets (database) → Slack/Email (output)
3434
- **Fee amount**: 10,000 KRW/month (MVP constant)
3535

3636
## Google Sheets Schema
37-
- `members`: id, name, email, school_email, phone, status, track, team, join_date, payment_status, last_updated
37+
- `members`: id, name, email, department, student_id, school_email, phone, status, track, team, join_date, payment_status, last_updated
3838
- `fees`: id, member_id, amount, paid_date, payment_method, notes, semester, last_updated
3939
- `groups`: id, name, type, parent_id, size, leader_email, last_updated
4040
- `events`: id, title, date, type, organizer, attendees, notes

docker-compose.yml

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/bcsd_api/auth/router.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ def post_register(
6969
sheets: SheetsClient = Depends(get_sheets),
7070
) -> LoginResponse:
7171
token = service.register(
72-
body.google_token, body.name, body.school_email,
72+
body.google_token, body.name, body.department,
73+
body.student_id, body.school_email,
7374
body.phone, body.track, settings, sheets,
7475
)
7576
_set_cookie(response, token, settings)

src/bcsd_api/auth/schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class ConfirmEmailResponse(BaseModel):
2626
class RegisterRequest(BaseModel):
2727
google_token: str
2828
name: str
29+
department: str
30+
student_id: str
2931
school_email: str
3032
phone: str
3133
track: str

src/bcsd_api/auth/service.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timezone, timedelta
1+
from datetime import datetime
22

33
from bcsd_api.config import Settings
44
from bcsd_api.email.sender import EmailSender
@@ -9,8 +9,7 @@
99
from . import google as google_auth
1010
from . import token as jwt_token
1111
from . import verify
12-
13-
_KST = timezone(timedelta(hours=9))
12+
from bcsd_api.timezone import KST
1413

1514

1615
def login(google_token: str, settings: Settings, sheets: SheetsClient) -> str:
@@ -33,6 +32,8 @@ def confirm_verify(email: str, code: str) -> bool:
3332
def register(
3433
google_token: str,
3534
name: str,
35+
department: str,
36+
student_id: str,
3637
school_email: str,
3738
phone: str,
3839
track: str,
@@ -42,7 +43,10 @@ def register(
4243
profile = google_auth.verify_token(google_token, settings.google_client_id)
4344
_check_duplicate(profile["email"], sheets)
4445
member_id = generate_id("M")
45-
row = _build_row(member_id, name, profile["email"], school_email, phone, track)
46+
row = _build_row(
47+
member_id, name, profile["email"],
48+
department, student_id, school_email, phone, track,
49+
)
4650
sheets.append_row("members", row)
4751
payload = {"sub": member_id, "email": profile["email"]}
4852
return _issue_jwt(payload, settings)
@@ -61,15 +65,20 @@ def _check_duplicate(email: str, sheets: SheetsClient) -> None:
6165

6266

6367
def _now_kst() -> str:
64-
return datetime.now(_KST).strftime("%Y-%m-%d %H:%M:%S")
68+
return datetime.now(KST).strftime("%Y-%m-%d %H:%M:%S")
6569

6670

6771
def _build_row(
68-
member_id: str, name: str, email: str, school_email: str, phone: str, track: str
72+
member_id: str, name: str, email: str,
73+
department: str, student_id: str,
74+
school_email: str, phone: str, track: str,
6975
) -> dict:
7076
now = _now_kst()
7177
base = _base_fields(member_id, name, email)
72-
extra = {"school_email": school_email, "phone": phone, "track": track}
78+
extra = {
79+
"department": department, "student_id": student_id,
80+
"school_email": school_email, "phone": phone, "track": track,
81+
}
7382
timestamps = {"join_date": now, "last_updated": now}
7483
return {**base, **extra, **timestamps}
7584

src/bcsd_api/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ class Settings(BaseSettings):
1313
cors_origins: str = "http://localhost:3000"
1414
cookie_name: str = "access_token"
1515
cookie_secure: bool = False
16-
spicedb_endpoint: str = "localhost:50051"
16+
spicedb_endpoint: str = "spicedb:50051"
1717
spicedb_token: str = "bcsd-dev-token"
1818
model_config = {"env_file": ".env"}

src/bcsd_api/dependencies.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,39 +17,31 @@ def get_settings() -> Settings:
1717
return Settings()
1818

1919

20-
_sheets_cache: SheetsClient | None = None
20+
@lru_cache
21+
def _create_sheets(credentials: str, sheets_id: str) -> SheetsClient:
22+
return SheetsClient(credentials, sheets_id)
2123

2224

2325
def get_sheets(settings: Settings = Depends(get_settings)) -> SheetsClient:
24-
global _sheets_cache
25-
if _sheets_cache:
26-
return _sheets_cache
27-
_sheets_cache = SheetsClient(
28-
settings.google_service_account_file, settings.google_sheets_id
29-
)
30-
return _sheets_cache
26+
return _create_sheets(settings.google_service_account_file, settings.google_sheets_id)
3127

3228

33-
_sender_cache: EmailSender | None = None
29+
@lru_cache
30+
def _create_sender(api_key: str, sender: str) -> EmailSender:
31+
return ResendSender(api_key, sender)
3432

3533

3634
def get_email_sender(settings: Settings = Depends(get_settings)) -> EmailSender:
37-
global _sender_cache
38-
if _sender_cache:
39-
return _sender_cache
40-
_sender_cache = ResendSender(settings.resend_api_key, settings.resend_sender)
41-
return _sender_cache
35+
return _create_sender(settings.resend_api_key, settings.resend_sender)
4236

4337

44-
_authz_cache: AuthzClient | None = None
38+
@lru_cache
39+
def _create_authz(endpoint: str, token: str) -> AuthzClient:
40+
return AuthzClient(endpoint, token)
4541

4642

4743
def get_authz(settings: Settings = Depends(get_settings)) -> AuthzClient:
48-
global _authz_cache
49-
if _authz_cache:
50-
return _authz_cache
51-
_authz_cache = AuthzClient(settings.spicedb_endpoint, settings.spicedb_token)
52-
return _authz_cache
44+
return _create_authz(settings.spicedb_endpoint, settings.spicedb_token)
5345

5446

5547
def get_member_repo(sheets: SheetsClient = Depends(get_sheets)) -> MemberRepository:

src/bcsd_api/id_gen.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import random
22
import string
3-
from datetime import datetime, timezone, timedelta
3+
from datetime import datetime
44

5-
_KST = timezone(timedelta(hours=9))
5+
from .timezone import KST
66

77

88
def generate_id(prefix: str) -> str:
9-
now = datetime.now(_KST)
9+
now = datetime.now(KST)
1010
timestamp = now.strftime("%Y%m%d%H%M%S")
1111
suffix = "".join(random.choices(string.ascii_uppercase + string.digits, k=3))
1212
return f"{prefix}-{timestamp}-{suffix}"

src/bcsd_api/main.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ def _init_spicedb(settings) -> None:
3030
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
3131
settings = get_settings()
3232
sheets = get_sheets(settings)
33-
sheets.init_sheets()
34-
from .sheets.defaults import seed
35-
seed(sheets)
33+
from .sheets.migrate import run as run_migrations
34+
run_migrations(sheets.spreadsheet)
3635
try:
3736
_init_spicedb(settings)
3837
except Exception:

src/bcsd_api/member/router.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
@router.get("/filters", response_model=FiltersResponse)
1616
def get_filters(
17-
_: dict = Depends(current_user),
1817
sheets: SheetsClient = Depends(get_sheets),
1918
) -> FiltersResponse:
2019
return service.get_filters(sheets)

0 commit comments

Comments
 (0)