Flow improvements

This commit is contained in:
2026-06-29 07:15:01 -04:00
parent 8d8cb9ccad
commit 0a49dfe219
16 changed files with 281 additions and 102 deletions
@@ -0,0 +1,34 @@
"""Add LOCAL status to PPR records
Revision ID: 011_ppr_local_status
Revises: 010_drone_request_agl_altitude
Create Date: 2026-06-29 00:00:00.000000
"""
from alembic import op
revision = '011_ppr_local_status'
down_revision = '010_drone_request_agl_altitude'
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("ALTER TABLE submitted CHANGE COLUMN departed_dt takeoff_dt DATETIME NULL")
op.execute("ALTER TABLE submitted ADD COLUMN qsy_dt DATETIME NULL AFTER takeoff_dt")
op.execute(
"ALTER TABLE submitted MODIFY COLUMN status "
"ENUM('NEW','CONFIRMED','CANCELED','LANDED','LOCAL','DELETED','DEPARTED','ACTIVATED') "
"NOT NULL DEFAULT 'NEW'"
)
def downgrade() -> None:
op.execute("UPDATE submitted SET status = 'LANDED' WHERE status = 'LOCAL'")
op.execute("ALTER TABLE submitted DROP COLUMN qsy_dt")
op.execute("ALTER TABLE submitted CHANGE COLUMN takeoff_dt departed_dt DATETIME NULL")
op.execute(
"ALTER TABLE submitted MODIFY COLUMN status "
"ENUM('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED','ACTIVATED') "
"NOT NULL DEFAULT 'NEW'"
)
+1 -1
View File
@@ -564,7 +564,7 @@ async def bulk_log_movement(
else:
ppr.out_to = entry.to_location or ppr.out_to
ppr.pob_out = entry.pob or ppr.pob_out
ppr.departed_dt = timestamp
ppr.takeoff_dt = timestamp
if ppr.status not in (PPRStatus.DELETED, PPRStatus.CANCELED):
ppr.status = PPRStatus.DEPARTED
if entry.notes:
+11 -3
View File
@@ -222,13 +222,21 @@ async def update_ppr_status(
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
event_timestamp = None
if ppr.status == PPRStatus.LANDED and ppr.landed_dt:
event_timestamp = ppr.landed_dt.isoformat()
elif ppr.status == PPRStatus.LOCAL and ppr.takeoff_dt:
event_timestamp = ppr.takeoff_dt.isoformat()
elif ppr.status == PPRStatus.DEPARTED and ppr.qsy_dt:
event_timestamp = ppr.qsy_dt.isoformat()
await request.app.state.connection_manager.broadcast({
"type": "status_update",
"data": {
"id": ppr.id,
"ac_reg": ppr.ac_reg,
"status": ppr.status.value,
"timestamp": ppr.landed_dt.isoformat() if ppr.landed_dt else (ppr.departed_dt.isoformat() if ppr.departed_dt else None)
"timestamp": event_timestamp
}
})
@@ -348,7 +356,7 @@ async def update_ppr_public(
detail="Invalid or expired token"
)
# Only allow editing if not already processed
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.LOCAL, PPRStatus.DEPARTED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be edited at this stage"
@@ -373,7 +381,7 @@ async def cancel_ppr_public(
detail="Invalid or expired token"
)
# Only allow canceling if not already processed
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.LOCAL, PPRStatus.DEPARTED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be cancelled at this stage"
+2 -1
View File
@@ -145,7 +145,8 @@ async def get_public_departures(db: Session = Depends(get_db)):
'ac_type': departure.ac_type,
'out_to': departure.out_to,
'etd': departure.etd,
'departed_dt': departure.departed_dt,
'takeoff_dt': departure.takeoff_dt,
'qsy_dt': departure.qsy_dt,
'status': departure.status.value,
'isLocalFlight': False,
'isDeparture': False
+5 -1
View File
@@ -58,6 +58,7 @@ class CRUDPPR:
PPRRecord.status == PPRStatus.NEW,
PPRRecord.status == PPRStatus.CONFIRMED,
PPRRecord.status == PPRStatus.LANDED,
PPRRecord.status == PPRStatus.LOCAL,
PPRRecord.status == PPRStatus.DEPARTED
)
)
@@ -71,6 +72,7 @@ class CRUDPPR:
func.date(PPRRecord.etd) == today,
or_(
PPRRecord.status == PPRStatus.LANDED,
PPRRecord.status == PPRStatus.LOCAL,
PPRRecord.status == PPRStatus.DEPARTED
)
)
@@ -151,8 +153,10 @@ class CRUDPPR:
current_time = timestamp if timestamp is not None else datetime.utcnow()
if status == PPRStatus.LANDED:
db_obj.landed_dt = current_time
elif status == PPRStatus.LOCAL:
db_obj.takeoff_dt = current_time
elif status == PPRStatus.DEPARTED:
db_obj.departed_dt = current_time
db_obj.qsy_dt = current_time
db.add(db_obj)
db.commit()
+3 -1
View File
@@ -9,6 +9,7 @@ class PPRStatus(str, Enum):
CONFIRMED = "CONFIRMED"
CANCELED = "CANCELED"
LANDED = "LANDED"
LOCAL = "LOCAL"
DELETED = "DELETED"
DEPARTED = "DEPARTED"
ACTIVATED = "ACTIVATED"
@@ -40,7 +41,8 @@ class PPRRecord(Base):
phone = Column(String(16), nullable=True)
notes = Column(Text, nullable=True)
landed_dt = Column(DateTime, nullable=True)
departed_dt = Column(DateTime, nullable=True)
takeoff_dt = Column(DateTime, nullable=True)
qsy_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True)
submitted_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
acknowledged_dt = Column(DateTime, nullable=True)
+5 -2
View File
@@ -9,6 +9,7 @@ class PPRStatus(str, Enum):
CONFIRMED = "CONFIRMED"
CANCELED = "CANCELED"
LANDED = "LANDED"
LOCAL = "LOCAL"
DELETED = "DELETED"
DEPARTED = "DEPARTED"
ACTIVATED = "ACTIVATED"
@@ -85,7 +86,8 @@ class PPRInDBBase(PPRBase):
id: int
status: PPRStatus
landed_dt: Optional[datetime] = None
departed_dt: Optional[datetime] = None
takeoff_dt: Optional[datetime] = None
qsy_dt: Optional[datetime] = None
created_by: Optional[str] = None
submitted_dt: datetime
acknowledged_dt: Optional[datetime] = None
@@ -111,7 +113,8 @@ class PPRPublic(BaseModel):
out_to: Optional[str] = None
etd: Optional[datetime] = None
landed_dt: Optional[datetime] = None
departed_dt: Optional[datetime] = None
takeoff_dt: Optional[datetime] = None
qsy_dt: Optional[datetime] = None
submitted_dt: datetime
class Config:
+3 -2
View File
@@ -81,7 +81,7 @@ DROP TABLE IF EXISTS `submitted`;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `submitted` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`status` enum('NEW','CONFIRMED','CANCELED','LANDED','DELETED','DEPARTED') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NEW',
`status` enum('NEW','CONFIRMED','CANCELED','LANDED','LOCAL','DELETED','DEPARTED','ACTIVATED') CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT 'NEW',
`ac_reg` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
`ac_type` varchar(32) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
`ac_call` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
@@ -97,7 +97,8 @@ CREATE TABLE `submitted` (
`phone` varchar(16) DEFAULT NULL,
`notes` varchar(2000) DEFAULT NULL,
`landed_dt` datetime DEFAULT NULL,
`departed_dt` datetime DEFAULT NULL,
`takeoff_dt` datetime DEFAULT NULL,
`qsy_dt` datetime DEFAULT NULL,
`created_by` varchar(16) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
`submitted_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `id` (`id`)
+28
View File
@@ -43,6 +43,34 @@ def test_authenticated_user_can_create_read_update_and_audit_ppr(auth_client, pp
assert any("Status changed from NEW to LANDED" in entry for entry in entries)
def test_ppr_departure_lifecycle_goes_landed_local_departed(auth_client, ppr_payload):
created = auth_client.post("/api/v1/pprs/", json=ppr_payload).json()
landed_response = auth_client.patch(
f"/api/v1/pprs/{created['id']}/status",
json={"status": "LANDED", "timestamp": "2026-06-20T10:30:00"},
)
local_response = auth_client.patch(
f"/api/v1/pprs/{created['id']}/status",
json={"status": "LOCAL", "timestamp": "2026-06-20T12:55:00"},
)
departed_response = auth_client.patch(
f"/api/v1/pprs/{created['id']}/status",
json={"status": "DEPARTED", "timestamp": "2026-06-20T13:05:00"},
)
assert landed_response.status_code == 200
assert local_response.status_code == 200
assert local_response.json()["status"] == "LOCAL"
assert local_response.json()["landed_dt"] == "2026-06-20T10:30:00"
assert local_response.json()["takeoff_dt"] == "2026-06-20T12:55:00"
assert local_response.json()["qsy_dt"] is None
assert departed_response.status_code == 200
assert departed_response.json()["status"] == "DEPARTED"
assert departed_response.json()["takeoff_dt"] == "2026-06-20T12:55:00"
assert departed_response.json()["qsy_dt"] == "2026-06-20T13:05:00"
def test_ppr_list_supports_status_date_and_pagination_filters(auth_client, ppr_factory):
ppr_factory(
ac_reg="G-NEW1",