Files
City10/app/Http/Controllers/Web/CityMarketController.php
marcocosta10000 c299a4ae21 feat(city): implement marketplace, missions, customization and admin dashboard
- Add database migrations for market_listings, missions and character cosmetics
- Create controllers for marketplace, missions, presence and character customization
- Implement admin world dashboard with live platform metrics
- Add feature tests for world functionality
- Include phase 2 design documents and gap analysis
2026-10-17 23:21:23 -03:00

181 lines
6.1 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Character;
use App\Models\InventoryItem;
use App\Models\MarketListing;
use App\Models\Mission;
use App\Models\Transaction;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Marketplace between citizens and mission journal endpoints.
*/
class CityMarketController extends Controller
{
/**
* Active listings in the caller's city plus own listings.
*/
public function index(Request $request): JsonResponse
{
$me = Character::ensureForUser($request->user());
$listings = MarketListing::query()
->where('city_id', $me->city_id)
->where('is_active', true)
->with('seller:id,name')
->orderByDesc('id')
->limit(50)
->get()
->map(fn (MarketListing $l): array => [
'id' => $l->id,
'item' => $l->item,
'qty' => (int) $l->qty,
'unit_price' => (float) $l->unit_price,
'seller_id' => $l->seller_character_id,
'seller_name' => $l->seller?->name,
'is_mine' => (int) $l->seller_character_id === (int) $me->id,
]);
$missions = Mission::query()
->where('city_id', $me->city_id)
->where('is_active', true)
->get()
->map(function (Mission $m) use ($me) {
$pivot = DB::table('character_missions')
->where('character_id', $me->id)
->where('mission_id', $m->id)
->first();
return [
'id' => $m->id,
'code' => $m->code,
'title' => $m->title,
'description' => $m->description,
'target' => (int) $m->target,
'reward' => (float) $m->reward,
'progress' => (int) ($pivot->progress ?? 0),
'is_completed' => (bool) ($pivot->is_completed ?? false),
];
});
return response()->json([
'listings' => $listings,
'missions' => $missions,
'money' => (float) $me->money,
'inventory' => $me->inventory()->get()->mapWithKeys(fn ($i) => [$i->item => (int) $i->qty])->all(),
]);
}
/**
* Publish inventory for sale.
*/
public function sell(Request $request): JsonResponse
{
$me = Character::ensureForUser($request->user());
$data = $request->validate([
'item' => ['required', 'in:food,water,materials,product'],
'qty' => ['required', 'integer', 'min:1', 'max:100'],
'unit_price' => ['required', 'numeric', 'min:0.5', 'max:1000'],
]);
$row = InventoryItem::query()
->where('character_id', $me->id)
->where('item', $data['item'])
->first();
if ($row === null || (int) $row->qty < (int) $data['qty']) {
return response()->json(['message' => __('city.no_inventory')], 422);
}
DB::transaction(function () use ($me, $data, $row): void {
$row->qty = (int) $row->qty - (int) $data['qty'];
$row->save();
MarketListing::create([
'seller_character_id' => $me->id,
'city_id' => $me->city_id,
'item' => $data['item'],
'qty' => (int) $data['qty'],
'unit_price' => (float) $data['unit_price'],
'is_active' => true,
]);
});
return response()->json(['message' => __('city.market_listed')]);
}
/**
* Purchase a listing from another citizen.
*/
public function buy(Request $request): JsonResponse
{
$me = Character::ensureForUser($request->user());
$data = $request->validate([
'listing_id' => ['required', 'integer', 'exists:market_listings,id'],
'qty' => ['nullable', 'integer', 'min:1', 'max:100'],
]);
$listing = MarketListing::query()->findOrFail($data['listing_id']);
if (! $listing->is_active || (int) $listing->seller_character_id === (int) $me->id) {
return response()->json(['message' => __('city.market_unavailable')], 422);
}
$qty = min((int) ($data['qty'] ?? $listing->qty), (int) $listing->qty);
$total = round($listing->unit_price * $qty, 2);
if ((float) $me->money < $total) {
return response()->json(['message' => __('city.no_money')], 422);
}
DB::transaction(function () use ($me, $listing, $qty, $total): void {
$seller = Character::query()->lockForUpdate()->findOrFail($listing->seller_character_id);
$me->money = round((float) $me->money - $total, 2);
$me->save();
$seller->money = round((float) $seller->money + $total, 2);
$seller->save();
$row = InventoryItem::query()
->where('character_id', $me->id)
->where('item', $listing->item)
->first();
if ($row) {
$row->qty = (int) $row->qty + $qty;
$row->save();
} else {
InventoryItem::create([
'character_id' => $me->id,
'item' => $listing->item,
'qty' => $qty,
]);
}
$listing->qty = (int) $listing->qty - $qty;
if ($listing->qty <= 0) {
$listing->is_active = false;
}
$listing->save();
Transaction::create([
'from_character_id' => $me->id,
'to_character_id' => $seller->id,
'item' => $listing->item,
'qty' => $qty,
'amount' => $total,
]);
});
$me->refresh();
return response()->json([
'message' => __('city.market_bought'),
'money' => (float) $me->money,
'inventory' => $me->inventory()->get()->mapWithKeys(fn ($i) => [$i->item => (int) $i->qty])->all(),
]);
}
}