Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -295,3 +295,6 @@ dist
pyrightconfig.json

opt/

# google api
server/api/booking/google_calendar/google_calendar_service.json
5 changes: 5 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ AWS_SECRET_ACCESS_KEY=xxxx
AWS_STORAGE_BUCKET_NAME=bucket_name
AWS_REGION_NAME=ap-southeast-2

# Google API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

GOOGLE_CREDENTIALS_FILE=server/api/booking/google_calendar/template_service_account.json
Copy link
Contributor

@ErikaKK ErikaKK Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GOOGLE_CREDENTIALS_FILE=api/booking/google_calendar/google_calendar_service.json

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also change the file name of template_service_account.json to google_calendar_service.example.json

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

GOOGLE_CALENDAR_ID=xxxx


Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary blank line added at the end of the Google API section.

Suggested change

Copilot uses AI. Check for mistakes.
FRONTEND_URL="http://localhost:3000"
Empty file added server/api/booking/__init__.py
Empty file.
Empty file.
29 changes: 29 additions & 0 deletions server/api/booking/google_calendar/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
from pathlib import Path
from google.oauth2 import service_account
from googleapiclient.discovery import build
from dotenv import load_dotenv


# Resolve BASE_DIR and load .env
# adjust to server root
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
load_dotenv(os.path.join(BASE_DIR, ".env"))

cred_path = os.getenv("GOOGLE_CREDENTIALS_FILE")
calendar_id = os.getenv("GOOGLE_CALENDAR_ID")
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calendar_id variable is retrieved but never used in this module. Consider removing it to avoid confusion.

Suggested change
calendar_id = os.getenv("GOOGLE_CALENDAR_ID")

Copilot uses AI. Check for mistakes.


def get_calendar_service():
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment variables are being read at module import time (line 13-14), which means they are loaded once when the module is first imported. This can cause issues in testing scenarios or when environment variables change during runtime. Consider moving these reads inside the get_calendar_service() function or making them lazy-loaded.

Suggested change
cred_path = os.getenv("GOOGLE_CREDENTIALS_FILE")
calendar_id = os.getenv("GOOGLE_CALENDAR_ID")
def get_calendar_service():
def get_calendar_service():
cred_path = os.getenv("GOOGLE_CREDENTIALS_FILE")
calendar_id = os.getenv("GOOGLE_CALENDAR_ID")

Copilot uses AI. Check for mistakes.

if not cred_path:
raise FileNotFoundError(
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using FileNotFoundError is misleading here. The error is raised when the environment variable is not set, not when a file is not found. The file might exist but the environment variable is simply missing. Consider using ValueError or a custom exception instead, as this is a configuration error.

Suggested change
raise FileNotFoundError(
raise ValueError(

Copilot uses AI. Check for mistakes.
"GOOGLE_CREDENTIALS_FILE is missing or invalid. "
f"Checked: {os.path.join(BASE_DIR, '.env')}"
)

creds = service_account.Credentials.from_service_account_file(
cred_path,
scopes=["https://www.googleapis.com/auth/calendar"]
)
return build("calendar", "v3", credentials=creds)
87 changes: 87 additions & 0 deletions server/api/booking/google_calendar/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import os
from .client import get_calendar_service

CALENDAR_ID = os.getenv("GOOGLE_CALENDAR_ID")
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CALENDAR_ID is loaded at module import time, which means it's evaluated once when the module is first imported. This can cause issues in testing environments or when environment variables change. Consider loading it inside each function or using a lazy-loading pattern.

Copilot uses AI. Check for mistakes.


def create_event(event_data: dict):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add docs for event_data's key

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"""
Create a new Google Calendar event.
**Expected event_data keys (Google Calendar API format):**
- summary (str): Title of the event.
- description (str, optional): Details about the event.
- location (str, optional): Physical or virtual location.
- start (dict): Start date/time.
Example:
{
"dateTime": "2025-02-01T10:00:00+08:00",
"timeZone": "Australia/Perth"
}
- end (dict): End date/time.
Example:
{
"dateTime": "2025-02-01T11:00:00+08:00",
"timeZone": "Australia/Perth"
}
Returns:
dict: The created event object from Google Calendar.
"""
if not CALENDAR_ID:
raise ValueError(
"GOOGLE_CALENDAR_ID is missing. Set it in your environment.")
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CALENDAR_ID validation check is duplicated across all four functions (lines 30, 48, 66, 83). Consider extracting this validation into a helper function or decorator to reduce code duplication and improve maintainability.

Copilot uses AI. Check for mistakes.

service = get_calendar_service()
return service.events().insert(calendarId=CALENDAR_ID, body=event_data).execute()


def get_event(event_id: str):
"""
Retrieve a specific event.
Args:
event_id (str): Google Calendar event ID.
Returns:
dict: Event details.
"""
if not CALENDAR_ID:
raise ValueError(
"GOOGLE_CALENDAR_ID is missing. Set it in your environment.")
service = get_calendar_service()
return service.events().get(calendarId=CALENDAR_ID, eventId=event_id).execute()


def update_event(event_id: str, updated_data: dict):
"""
Update an existing event.
Args:
event_id (str): Google Calendar event ID.
updated_data (dict): Same structure as event_data.
Returns:
dict: Updated event object.
"""
if not CALENDAR_ID:
raise ValueError(
"GOOGLE_CALENDAR_ID is missing. Set it in your environment.")
service = get_calendar_service()
return service.events().update(calendarId=CALENDAR_ID, eventId=event_id, body=updated_data).execute()


def delete_event(event_id: str):
"""
Delete an event.
Args:
event_id (str): Google Calendar event ID.
Returns:
dict: Response from Google Calendar API (usually empty).
"""
if not CALENDAR_ID:
raise ValueError(
"GOOGLE_CALENDAR_ID is missing. Set it in your environment.")
service = get_calendar_service()
return service.events().delete(calendarId=CALENDAR_ID, eventId=event_id).execute()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "service_account",
"project_id": "xxx",
"private_key_id": "xxx",
"private_key": "-----BEGIN PRIVATE KEY-----\nxxx\n-----END PRIVATE KEY-----\n",
"client_email": "[email protected]",
"client_id": "xxx"
}
44 changes: 44 additions & 0 deletions server/api/booking/google_calendar/test_google_calendar.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test fails when you don't set the environment variables so need to fix it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest
import os
from api.booking.google_calendar.events import (
create_event,
get_event,
update_event,
delete_event,
)
# skip if no Google Calendar env variables are set
pytestmark = pytest.mark.skipif(
not os.getenv("GOOGLE_CREDENTIALS_FILE") or not os.getenv(
"GOOGLE_CALENDAR_ID"),
reason="Google Calendar integration env vars missing. Set GOOGLE_CREDENTIALS_FILE and GOOGLE_CALENDAR_ID to run this test."
)


def test_google_calendar_crud():

event_data = {
"summary": "Test Event",
"description": "CRUD test event",
"start": {
"dateTime": "2025-12-2T15:00:00",
"timeZone": "Australia/Perth",
},
"end": {
"dateTime": "2025-12-2T16:00:00",
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date format in the test is invalid. ISO 8601 format requires two-digit day values. The date "2025-12-2" should be "2025-12-02". This applies to both the start and end dateTime fields.

Suggested change
"dateTime": "2025-12-2T15:00:00",
"timeZone": "Australia/Perth",
},
"end": {
"dateTime": "2025-12-2T16:00:00",
"dateTime": "2025-12-02T15:00:00",
"timeZone": "Australia/Perth",
},
"end": {
"dateTime": "2025-12-02T16:00:00",

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date format in the test is invalid. ISO 8601 format requires two-digit day values. The date "2025-12-2" should be "2025-12-02".

Suggested change
"dateTime": "2025-12-2T15:00:00",
"timeZone": "Australia/Perth",
},
"end": {
"dateTime": "2025-12-2T16:00:00",
"dateTime": "2025-12-02T15:00:00",
"timeZone": "Australia/Perth",
},
"end": {
"dateTime": "2025-12-02T16:00:00",

Copilot uses AI. Check for mistakes.
"timeZone": "Australia/Perth",
},
}

created = create_event(event_data)
event_id = created["id"]

fetched = get_event(event_id)
assert fetched["summary"] == "Test Event"

updated_data = dict(event_data)
updated_data["summary"] = "Updated Test Event"

updated = update_event(event_id, updated_data)
assert updated["summary"] == "Updated Test Event"

delete_event(event_id)
Loading