diff --git a/crud.py b/crud.py index f8137f5..67d2042 100644 --- a/crud.py +++ b/crud.py @@ -18,8 +18,7 @@ async def create_schedule(wallet_id: str, data: CreateSchedule) -> Schedule: schedule_id = urlsafe_short_hash() - # hardcode timeslot for now - timeslot = 30 + schedule = Schedule( id=schedule_id, wallet=wallet_id, @@ -29,7 +28,8 @@ async def create_schedule(wallet_id: str, data: CreateSchedule) -> Schedule: start_time=data.start_time, end_time=data.end_time, amount=data.amount, - timeslot=timeslot, + timeslot=data.timeslot, + currency=data.currency, ) await db.execute( insert_query("lncalendar.schedule", schedule), @@ -92,6 +92,12 @@ async def create_appointment( ) return appointment +async def update_appointment(appointment: Appointment) -> Appointment: + await db.execute( + update_query("lncalendar.appointment", appointment), + appointment.dict(), + ) + return appointment async def get_appointment(appointment_id: str) -> Optional[Appointment]: row = await db.fetchone( diff --git a/migrations.py b/migrations.py index c17ada5..0f449f1 100644 --- a/migrations.py +++ b/migrations.py @@ -1,3 +1,8 @@ +from sqlalchemy.exc import OperationalError + +from lnbits.helpers import insert_query + + async def m001_initial(db): """ Initial schedules table. @@ -70,8 +75,6 @@ async def m002_rename_time_to_created_at(db): """ ) - -async def m003_add_unavailable_name(db): """ Add name to the unavailable table. """ @@ -82,8 +85,6 @@ async def m003_add_unavailable_name(db): """ ) - -async def m004_add_timeslot(db): """ Add timeslot to the schedule table. """ @@ -94,8 +95,6 @@ async def m004_add_timeslot(db): """ ) - -async def m005_add_nostr_pubkey(db): """ Add nostr_pubkey to the appointment table. """ @@ -105,3 +104,40 @@ async def m005_add_nostr_pubkey(db): ADD COLUMN nostr_pubkey TEXT; """ ) + + +async def m003_add_fiat_currency(db): + """ + Add currency to schedule to allow fiat denomination + of appointments. Make price a float. + """ + try: + await db.execute( + "ALTER TABLE lncalendar.schedule RENAME TO schedule_backup;") + await db.execute( + """ + CREATE TABLE lncalendar.schedule ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + start_day INTEGER NOT NULL, + end_day INTEGER NOT NULL, + start_time TEXT NOT NULL, + end_time TEXT NOT NULL, + amount FLOAT NOT NULL, + timeslot INTEGER NOT NULL DEFAULT 30, + currency TEXT NOT NULL DEFAULT 'sat' + ); + """ + ) + + await db.execute( + """ + INSERT INTO lncalendar.schedule (id, wallet, name, start_day, end_day, start_time, end_time, amount, timeslot) + SELECT id, wallet, name, start_day, end_day, start_time, end_time, amount, timeslot FROM lncalendar.schedule_backup; + """ + ) + + await db.execute("DROP TABLE lncalendar.schedule_backup;") + except OperationalError: + pass diff --git a/models.py b/models.py index b1b4031..f5a127b 100644 --- a/models.py +++ b/models.py @@ -12,8 +12,9 @@ class CreateSchedule(BaseModel): end_day: int = Query(..., ge=0, le=6) start_time: str = Query(...) end_time: str = Query(...) - amount: int = Query(..., ge=1) - timeslot: int = Query(..., ge=15) + amount: float = Query(..., ge=0) + timeslot: int = Query(30, ge=5) + currency: str = Query('sat') class CreateUnavailableTime(BaseModel): @@ -32,6 +33,15 @@ class CreateAppointment(BaseModel): end_time: str = Query(...) schedule: str = Query(...) +class UpdateAppointment(BaseModel): + name: Optional[str] = Query(None) + email: Optional[str] = Query(None) + nostr_pubkey: Optional[str] = Query(None) + info: Optional[str] = Query(None) + start_time: Optional[str] = Query(None) + end_time: Optional[str] = Query(None) + schedule: Optional[str] = Query(None) + class Schedule(BaseModel): id: str @@ -41,8 +51,9 @@ class Schedule(BaseModel): end_day: int start_time: str end_time: str - amount: int + amount: float timeslot: int + currency: str @property def availabe_days(self): diff --git a/tasks.py b/tasks.py index 620c488..e09ee22 100644 --- a/tasks.py +++ b/tasks.py @@ -5,6 +5,7 @@ from lnbits.core.models import Payment from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener +from lnbits.utils.exchange_rates import fiat_amount_as_satoshis from .crud import get_appointment, get_schedule, set_appointment_paid @@ -30,5 +31,14 @@ async def on_invoice_paid(payment: Payment) -> None: schedule = await get_schedule(appointment.schedule) assert schedule - if payment.amount == schedule.amount * 1000: + price = ( + schedule.amount * 1000 + if schedule.currency == "sat" + else await fiat_amount_as_satoshis(schedule.amount, schedule.currency) + * 1000 + ) + + lower_bound = price * 0.99 # 1% decrease + + if abs(payment.amount) >= lower_bound: # allow 1% error await set_appointment_paid(payment.payment_hash) diff --git a/views_api.py b/views_api.py index ef62136..4e8aae9 100644 --- a/views_api.py +++ b/views_api.py @@ -2,13 +2,17 @@ from fastapi import APIRouter, Depends, Query from fastapi.exceptions import HTTPException -from httpx import delete -from lnbits import app from lnbits.core.crud import get_standalone_payment, get_user from lnbits.core.models import User, WalletTypeInfo from lnbits.core.services import create_invoice from lnbits.decorators import check_user_exists, require_admin_key, require_invoice_key +from lnbits.utils.exchange_rates import ( + allowed_currencies, + fiat_amount_as_satoshis, + get_fiat_rate_satoshis, + satoshis_amount_as_fiat, +) from .crud import ( create_appointment, @@ -25,9 +29,15 @@ get_unavailable_times, purge_appointments, set_appointment_paid, + update_appointment, update_schedule, ) -from .models import CreateAppointment, CreateSchedule, CreateUnavailableTime +from .models import ( + CreateAppointment, + CreateSchedule, + CreateUnavailableTime, + UpdateAppointment, +) lncalendar_api_router = APIRouter() @@ -129,9 +139,12 @@ async def api_appointment_create(data: CreateAppointment): status_code=HTTPStatus.NOT_FOUND, detail="Schedule does not exist." ) try: + amount = schedule.amount + if schedule.currency != "sat": + amount = await fiat_amount_as_satoshis(schedule.amount, schedule.currency) payment_hash, payment_request = await create_invoice( wallet_id=schedule.wallet, - amount=schedule.amount, # type: ignore + amount=amount, # type: ignore memo=f"{schedule.name}", extra={"tag": "lncalendar", "name": data.name, "email": data.email}, ) @@ -145,6 +158,34 @@ async def api_appointment_create(data: CreateAppointment): return {"payment_hash": payment_hash, "payment_request": payment_request} +@lncalendar_api_router.put("/api/v1/appointment/{appointment_id}") +async def api_appointment_update( + appointment_id: str, + data: UpdateAppointment, + user: User = Depends(check_user_exists), +): + appointment = await get_appointment(appointment_id) + if not appointment: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Appointment does not exist." + ) + schedule = await get_schedule(appointment.schedule) + if not schedule: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Schedule does not exist." + ) + if schedule.wallet not in user.wallet_ids: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="Not your schedule." + ) + + for k, v in data.dict().items(): + if v is not None: + setattr(appointment, k, v) + + appointment = await update_appointment(appointment) + return appointment.dict() + @lncalendar_api_router.get("/api/v1/appointment/purge/{schedule_id}") async def api_purge_appointments(schedule_id: str): schedule = await get_schedule(schedule_id) @@ -258,3 +299,8 @@ async def api_unavailable_delete( ) await delete_unavailable_time(unavailable_id) return "", HTTPStatus.NO_CONTENT + +## Currency API +@lncalendar_api_router.get("/api/v1/currencies") +async def api_get_currencies(): + return allowed_currencies() \ No newline at end of file