Skip to content

[7.x] fix(simbrief): write OFP aircraft back to the user's bid#2232

Open
jordanhavard wants to merge 1 commit into
phpvms:releases/7.0from
jordanhavard:fix/simbrief-bid-aircraft-writeback
Open

[7.x] fix(simbrief): write OFP aircraft back to the user's bid#2232
jordanhavard wants to merge 1 commit into
phpvms:releases/7.0from
jordanhavard:fix/simbrief-bid-aircraft-writeback

Conversation

@jordanhavard

@jordanhavard jordanhavard commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

When a pilot generates a SimBrief OFP, the selected aircraft is saved only to the simbrief table — the user's bid for the same flight keeps a null (or stale) aircraft_id. The bid is read to seed the aircraft selection in SimBriefController::generate(), but the choice never flows back.

This adds a sync in SimBriefService::downloadOfp() (single choke point covering both initial generation via check_ofp() and OFP updates via update_ofp()):

  • If the user has a bid on the flight, its aircraft_id is synced to the aircraft the OFP was generated with (fills null, overwrites stale).
  • With bids.block_aircraft enabled, the sync is skipped (and logged) if another bid already holds the aircraft — mirroring BidService::addBid(), but non-fatal since the OFP already exists.
  • No bid is ever created by the sync; the briefing flow is never interrupted.

Test Plan

  • test_ofp_download_writes_aircraft_to_bid — bid with null aircraft gets populated
  • test_ofp_download_overwrites_stale_bid_aircraft — bid holding a different aircraft is synced to the OFP aircraft
  • test_ofp_download_skips_bid_sync_when_aircraft_blocked — with bids.block_aircraft, a bid is left untouched when another bid holds the aircraft
  • test_ofp_download_without_bid_creates_no_bid — no bid is created when none exists
  • Full SimBriefTest + BidTest suites pass

Summary by CodeRabbit

  • New Features

    • Flight planning operations now automatically synchronize aircraft assignments back to associated user bids, with built-in conflict prevention when aircraft are reserved by other bookings.
  • Tests

    • Added four new test cases validating aircraft synchronization behavior during flight planning, including successful syncs, aircraft updates, conflict prevention, and scenarios without associated bids.

The aircraft selected when generating a SimBrief OFP was only saved to
the simbrief table; the user's bid for the flight kept a null or stale
aircraft_id. Sync it in SimBriefService::downloadOfp() so the bid always
reflects the briefing, covering both initial generation (check_ofp) and
OFP updates (update_ofp).

When bids.block_aircraft is enabled and another bid already holds the
aircraft, the sync is skipped and logged, mirroring BidService::addBid().
No bid is ever created by the sync.
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds aircraft synchronization from SimBrief OFP generation to user bids. When an OFP is downloaded, the aircraft selected during briefing is written to the corresponding bid record, with a safeguard to prevent overwriting bids that hold aircraft blocked by configuration.

Changes

OFP Aircraft Synchronization

Layer / File(s) Summary
Bid-aircraft synchronization implementation
app/Services/SimBriefService.php
Imports Bid model and refactors downloadOfp() to capture the SimBrief upsert result, invoke syncBidAircraft() for bid synchronization, and return the stored record. New syncBidAircraft() method updates the matching bid's aircraft when not already set, with a safeguard to skip updates when bids.block_aircraft is enabled and another bid already holds the aircraft.
Bid synchronization test coverage
tests/SimBriefTest.php
Imports Bid model and adds four test cases: OFP download writes aircraft to an existing bid, overwrites a stale aircraft assignment, respects aircraft blocking when another bid holds the aircraft, and does not create bids when none exist for the flight.

Sequence Diagram

sequenceDiagram
  participant DownloadOFP as downloadOfp()
  participant SimBrief as SimBrief::updateOrCreate()
  participant SyncBid as syncBidAircraft()
  participant Bid as Bid query
  participant Setting as bids.block_aircraft
  
  DownloadOFP->>SimBrief: upsert OFP record
  SimBrief-->>DownloadOFP: SimBrief instance
  DownloadOFP->>SyncBid: sync with aircraft_id
  SyncBid->>Bid: find by user_id & flight_id
  Bid-->>SyncBid: bid record
  SyncBid->>Setting: check if block_aircraft enabled
  Setting-->>SyncBid: setting value
  SyncBid->>Bid: update aircraft_id (if allowed)
  SyncBid-->>DownloadOFP: sync complete
  DownloadOFP-->>DownloadOFP: return SimBrief
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A SimBrief brief flies through the air,
Its aircraft choice now settles fair,
The bid awaits with open arms,
Protected well from blocking charms,
From sky to bid, synchronization's care! ✈️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: syncing OFP aircraft selection back to the user's bid, which is the core objective of this fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.2.1)

PHPStan was skipped because the user-provided config is missing the required paths: directive.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/SimBriefTest.php (1)

332-360: ⚡ Quick win

Also assert the blocked sync stays non-fatal.

This test only proves the bid stays untouched. If downloadOfp() regressed to returning null after the skip, the test would still pass and the OFP-creation guarantee would go untested. Capture the returned briefing and assert it was persisted.

Suggested test tightening
-        $this->downloadOfp($this->user, $flight, $aircraft, []);
+        $briefing = $this->downloadOfp($this->user, $flight, $aircraft, []);
 
         $bid->refresh();
+        $this->assertNotNull($briefing);
         $this->assertNull($bid->aircraft_id);

Based on PR objectives, the blocked path is supposed to skip bid sync without interrupting briefing creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SimBriefTest.php` around lines 332 - 360, In
test_ofp_download_skips_bid_sync_when_aircraft_blocked, capture the return value
from downloadOfp($this->user, $flight, $aircraft, []) into a variable (e.g.
$briefing) and add an assertion that the briefing was persisted (e.g.
assertNotNull($briefing) or assertDatabaseHas for the briefing record) to ensure
downloadOfp() still creates/stores the briefing even when bid sync is skipped;
keep the existing bid->refresh(); assertNull($bid->aircraft_id) assertion
intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Services/SimBriefService.php`:
- Around line 127-137: The aircraft-blocking check in SimBriefService is racy
because Bid::where(...)->exists() and $bid->save() are separate; wrap the
check+assign in a DB transaction and acquire a row lock so the existence check
is done with a SELECT ... FOR UPDATE (or equivalent Eloquent lockForUpdate)
before assigning the aircraft_id and calling $bid->save(), matching the same
transactional/locked semantics used by the bid creation flow in BidService (use
the same lock/constraint logic and/or add a DB-level unique constraint on active
aircraft assignments if present in BidService); ensure the code paths reference
the same predicate for "active" bids and abort/return if the locked query finds
a conflicting bid.

In `@tests/SimBriefTest.php`:
- Around line 287-290: Add a Bid factory and update tests to use it: create
app/Database/factories/BidFactory.php and ensure the Bid model uses HasFactory
so tests can call Bid::factory()->create(...); replace the hand-rolled
Bid::create(...) usages in SimBriefTest with Bid::factory()->create([...])
(references: Bid model, BidFactory, SimBriefTest). Also in the
test_ofp_download_skips_bid_sync_when_aircraft_blocked test, add an assertion
that the return value of downloadOfp(...) is not null (downloadOfp) to ensure
the OFP was actually created/updated even when syncBidAircraft is skipped.

---

Nitpick comments:
In `@tests/SimBriefTest.php`:
- Around line 332-360: In
test_ofp_download_skips_bid_sync_when_aircraft_blocked, capture the return value
from downloadOfp($this->user, $flight, $aircraft, []) into a variable (e.g.
$briefing) and add an assertion that the briefing was persisted (e.g.
assertNotNull($briefing) or assertDatabaseHas for the briefing record) to ensure
downloadOfp() still creates/stores the briefing even when bid sync is skipped;
keep the existing bid->refresh(); assertNull($bid->aircraft_id) assertion
intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ce43357-aec8-47d9-b3f6-d451f98ed04c

📥 Commits

Reviewing files that changed from the base of the PR and between 46a0e88 and 1c30eeb.

📒 Files selected for processing (2)
  • app/Services/SimBriefService.php
  • tests/SimBriefTest.php

Comment on lines +127 to +137
if (setting('bids.block_aircraft')) {
$blocked = Bid::where('aircraft_id', $ac_id)->where('id', '!=', $bid->id)->exists();
if ($blocked) {
Log::info('SimBrief | Bid aircraft sync skipped, aircraft='.$ac_id.' is held by another bid');

return;
}
}

$bid->aircraft_id = $ac_id;
$bid->save();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the aircraft-blocking check atomic.

Line 128's exists() check and Line 137's save() are separate statements, so two concurrent OFP downloads can both see the aircraft as free and then assign it to different bids. That breaks the bids.block_aircraft guarantee under load. Please route this through a transactional/locked path shared with bid creation, or enforce the invariant with a database constraint on active aircraft assignments.

Based on PR objectives and app/Services/BidService.php:126-194, this path is expected to honor the same aircraft-blocking contract as bid creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Services/SimBriefService.php` around lines 127 - 137, The
aircraft-blocking check in SimBriefService is racy because
Bid::where(...)->exists() and $bid->save() are separate; wrap the check+assign
in a DB transaction and acquire a row lock so the existence check is done with a
SELECT ... FOR UPDATE (or equivalent Eloquent lockForUpdate) before assigning
the aircraft_id and calling $bid->save(), matching the same transactional/locked
semantics used by the bid creation flow in BidService (use the same
lock/constraint logic and/or add a DB-level unique constraint on active aircraft
assignments if present in BidService); ensure the code paths reference the same
predicate for "active" bids and abort/return if the locked query finds a
conflicting bid.

Comment thread tests/SimBriefTest.php
Comment on lines +287 to +290
$bid = Bid::create([
'user_id' => $this->user->id,
'flight_id' => $flight->id,
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

FACTORIES="$(fd -i 'BidFactory.php' || true)"

if [ -z "$FACTORIES" ]; then
  echo "BidFactory.php not found"
  exit 0
fi

printf 'Found Bid factory file(s):\n%s\n' "$FACTORIES"
rg -n -C2 'class\s+BidFactory|protected \$model\s*=.*Bid|function\s+\w+\(' $FACTORIES

Repository: phpvms/phpvms

Length of output: 82


🏁 Script executed:

#!/bin/bash
set -e

echo "== Locate potential Bid factory files =="
fd -a -i '*bid*factory*.php' . || true
fd -a -i 'Factory.php' database .github tests . || true
fd -a -i '*Factory*.php' database . || true

echo
echo "== Search for factory definitions/usage =="
rg -n "BidFactory|class\s+\w*Bid\w*Factory" -S .
rg -n "Bid::factory\(" -S .
rg -n "factory\(\s*Bid::" -S .
rg -n "database/factories|ModelFactory" -S .

echo
echo "== Inspect tests/SimBriefTest.php around mentioned lines =="
if [ -f tests/SimBriefTest.php ]; then
  sed -n '250,390p' tests/SimBriefTest.php | nl -ba | sed -n '1,200p'
else
  echo "tests/SimBriefTest.php not found"
fi

Repository: phpvms/phpvms

Length of output: 1996


🏁 Script executed:

#!/bin/bash
set -e

echo "== List factory files under app/Database/factories/"
ls -1 app/Database/factories 2>/dev/null | nl -ba || true

echo
echo "== Find any factory files that mention Bid in their filename/path =="
fd -a -i --glob '*bid*factory*.php' . || true
fd -a -i --glob '*bid*.php' app/Database/factories || true
fd -a -i --glob '*bid*.php' app/Database || true

echo
echo "== Search for Bid factory class/definition and Bid::factory usage =="
rg -n "class\s+\w*Bid\w*Factory|BidFactory" -S app . || true
rg -n "Bid::factory\(" -S . || true
rg -n "factory\(\s*Bid::" -S . || true

echo
echo "== Inspect tests/SimBriefTest.php around 270-380 =="
if [ -f tests/SimBriefTest.php ]; then
  sed -n '250,390p' tests/SimBriefTest.php | nl -ba
else
  echo "tests/SimBriefTest.php not found"
fi

Repository: phpvms/phpvms

Length of output: 675


🏁 Script executed:

#!/bin/bash
set -e

echo "== Check repo root =="
pwd
ls -la

echo
echo "== List app/Database/factories =="
ls -la app/Database/factories | head -n 50

echo
echo "== List any factory files that mention Bid in filename/path (glob) =="
fd -a -i --glob '*bid*factory*.php' . || true
fd -a -i --glob '*bid*.php' app/Database/factories app . || true

echo
echo "== Search for Bid factory class/definition and Bid::factory usage =="
rg -n "class\s+\w*Bid\w*Factory|BidFactory" app/Database/factories app database tests . || true
rg -n "Bid::factory\(" . || true
rg -n "->factory\(\)" app/Database/factories app tests . || true

echo
echo "== Inspect tests/SimBriefTest.php around 260-380 with line numbers =="
if [ -f tests/SimBriefTest.php ]; then
  sed -n '250,390p' tests/SimBriefTest.php | cat -n
else
  echo "tests/SimBriefTest.php not found"
fi

Repository: phpvms/phpvms

Length of output: 12156


🏁 Script executed:

#!/bin/bash
set -e

echo "== Inspect app/Models/Bid.php for HasFactory / factory mapping =="
if [ -f app/Models/Bid.php ]; then
  sed -n '1,200p' app/Models/Bid.php | cat
else
  echo "Missing app/Models/Bid.php"
fi

echo
echo "== Search for any Bid factory class/file =="
rg -n "class\s+\w*Bid\w*Factory|BidFactory" -S . || true
echo
echo "== Search for Bid::factory usage in repo =="
rg -n "Bid::factory\(" -S . || true

echo
echo "== Search for Bid::create usage in tests =="
rg -n "Bid::create\(" -S tests/ || true

Repository: phpvms/phpvms

Length of output: 1530


🏁 Script executed:

#!/bin/bash
set -e

echo "== Show exact sections around each Bid::create usage (with real line numbers) =="
for start in 270 300 330 340 345 350; do
  end=$((start+25))
  echo
  echo "--- tests/SimBriefTest.php:${start}-${end} ---"
  awk -v s=$start -v e=$end 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' tests/SimBriefTest.php
done

Repository: phpvms/phpvms

Length of output: 5594


🏁 Script executed:

#!/bin/bash
set -e
rg -n "function\s+downloadOfp\s*\(" tests/SimBriefTest.php
rg -n "downloadOfp\(" tests/SimBriefTest.php

Repository: phpvms/phpvms

Length of output: 896


🏁 Script executed:

#!/bin/bash
set -e
sed -n '1,130p' tests/SimBriefTest.php | cat -n

Repository: phpvms/phpvms

Length of output: 4767


🏁 Script executed:

#!/bin/bash
set -e

echo "== Locate SimBriefService::downloadOfp implementation =="
rg -n "function\s+downloadOfp\s*\(" app/Services/SimBriefService.php app -S

echo
echo "== Print downloadOfp() body in app/Services/SimBriefService.php =="
php_file="$(rg -l "function\s+downloadOfp\s*\(" app/Services/SimBriefService.php app -S | head -n 1 || true)"
if [ -n "$php_file" ]; then
  echo "Using: $php_file"
  # show a larger window around the first match
  match_line="$(rg -n "function\s+downloadOfp\s*\(" "$php_file" -S | head -n 1 | cut -d: -f1)"
  start=$((match_line-60))
  end=$((match_line+160))
  sed -n "${start},${end}p" "$php_file" | cat -n
else
  echo "No downloadOfp() found"
fi

echo
echo "== Search for bids.block_aircraft usage =="
rg -n "bids\.block_aircraft" app -S || true

echo
echo "== Search for aircraft_id/bid sync logic in service/listeners =="
rg -n "aircraft_id|sync|downloadOfp|block_aircraft" app/Services app/Listeners app/Support app/Models -S || true

Repository: phpvms/phpvms

Length of output: 9105


🏁 Script executed:

#!/bin/bash
set -e

echo "== Line numbers for downloadOfp() in app/Services/SimBriefService.php =="
rg -n "public function downloadOfp|private function syncBidAircraft" app/Services/SimBriefService.php

echo
echo "== Print app/Services/SimBriefService.php around downloadOfp() (1-220) =="
sed -n '1,220p' app/Services/SimBriefService.php | cat -n

echo
echo "== Print app/Services/BidService.php around block_aircraft logic (90-220) =="
sed -n '90,220p' app/Services/BidService.php | cat -n

Repository: phpvms/phpvms

Length of output: 14723


Add a Bid factory (or use one) in SimBriefTest

tests/SimBriefTest.php currently hand-rolls bids with Bid::create(...) (e.g., at ~287-290, and also ~314-318, ~343-347, ~352-355). The repo has no Bid factory (app/Database/factories/ contains no BidFactory.php, and app/Models/Bid.php has no HasFactory), so the tests can’t follow the “factory-backed model fixtures” guideline—add a BidFactory (with any needed factory state) and switch these fixtures to it.

For test_ofp_download_skips_bid_sync_when_aircraft_blocked, also assert the return value of downloadOfp(...) is not null (blocking only skips syncBidAircraft; downloadOfp() still returns the created/updated SimBrief), so a regression that fails the OFP download doesn’t get missed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SimBriefTest.php` around lines 287 - 290, Add a Bid factory and update
tests to use it: create app/Database/factories/BidFactory.php and ensure the Bid
model uses HasFactory so tests can call Bid::factory()->create(...); replace the
hand-rolled Bid::create(...) usages in SimBriefTest with
Bid::factory()->create([...]) (references: Bid model, BidFactory, SimBriefTest).
Also in the test_ofp_download_skips_bid_sync_when_aircraft_blocked test, add an
assertion that the return value of downloadOfp(...) is not null (downloadOfp) to
ensure the OFP was actually created/updated even when syncBidAircraft is
skipped.

Source: Coding guidelines

@jordanhavard

Copy link
Copy Markdown
Contributor Author

came across this one while working through another issue. Unsure if this is intentional or not, if it is, feel free to close :D

@nabeelio

nabeelio commented Jun 7, 2026

Copy link
Copy Markdown
Member

This syncs the user selected aircraft in sb to the right one in phpvms?

@arthurpar06

Copy link
Copy Markdown
Member

This PR syncs the SimBrief aircraft_id with the flight bid. Both reference phpVMS aircraft (nothing is related to the SB API)

A while back, we used to select the aircraft when generating a SimBrief OFP (which is why we have an aircraft_id field in the SimBrief table). However, this created a race condition where two people could select the same aircraft on the generation page before finalizing it, causing conflicts. To fix this, I added the ability to bid on an aircraft at the same time you bid on a flight.

This PR ensures that if an aircraft wasn't assigned during the initial flight bid, but was chosen while generating the OFP, it will now be added to the bid retroactively.

Omitting this from the first PR was intentional, as the aircraft is blocked whether it is recorded in the bid or the SimBrief table. I'm not against adding it now however you have to make sure you are using the right types: user_id and ac_id are supposed to be integers

@jordanhavard

Copy link
Copy Markdown
Contributor Author

This syncs the user selected aircraft in sb to the right one in phpvms?

Yeah, from testing, during the SB creation flow, you select an AC, this sends it to SB and then once that generation comes back, it then sets aircraft_id on the SB table - this PR then also sets that on the bids table too.

this created a race condition where two people could select the same aircraft on the generation page before finalizing it

Ahh makes sense. And I think this would reintroduce this issue. A couple options

  • On AC select pre generation, persist this to the bid.aircraft_id
  • Add an additional check in the pre simbrief step to ensure this aircraft is able to be booked (if that option is enabled)
    Con: SB failure leaves this in a bad state.
    or;
  • Aquire a lock on the AC during generation.

My gut says bids should be the authoritave table for this but also know that there is existing history here so happy with either direction here, let me know your thoughts and happy to amend the PR for another pass 😁

Thanks for the input 🙏

@arthurpar06

Copy link
Copy Markdown
Member

One feature that would make a lot of sense for v8 is requiring users to have a active bid before generating a SimBrief OFP. By doing this, we would only need to store the aircraft at the bid level, leveraging DB transactions and locks to entirely prevent race conditions.

While this would significantly streamline our codebase, it will slightly alter the current user workflow for those who aren't used to bidding before a flight. However, we can easily mitigate this in the UI: when a user clicks "Add Bid," a modal will open for aircraft selection, immediately followed by a redirection to the SimBrief generation page.

Needs to be discussed with/decided by @nabeelio

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants