diff --git a/src/Jobs/ClaimBeam.php b/src/Jobs/ClaimBeam.php index 7d9ddec..fa14506 100644 --- a/src/Jobs/ClaimBeam.php +++ b/src/Jobs/ClaimBeam.php @@ -4,7 +4,9 @@ use Enjin\Platform\Beam\Enums\BeamType; use Enjin\Platform\Beam\Enums\PlatformBeamCache; +use Enjin\Platform\Beam\Exceptions\BeamException; use Enjin\Platform\Beam\Models\BeamClaim; +use Enjin\Platform\Beam\Models\BeamPack; use Enjin\Platform\Beam\Models\BeamScan; use Enjin\Platform\Beam\Services\BatchService; use Enjin\Platform\Beam\Services\BeamService; @@ -13,6 +15,7 @@ use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -45,17 +48,23 @@ public function handle(BatchService $batch, WalletService $wallet): void try { $lock->block(5); - if ($claim = $this->claimQuery($data)->first()) { - DB::beginTransaction(); + $claims = $this->claims($data); + if (count($claims)) { $wallet->firstOrStore(['public_key' => $data['wallet_public_key']]); - $claim->forceFill($this->buildBeamClaimAttributes($batch, $claim))->save(); + $isPackUpdated = false; + DB::beginTransaction(); + foreach ($claims as $claim) { + $claim->forceFill($this->buildBeamClaimAttributes($batch, $claim))->save(); + Log::info('ClaimBeamJob: Claim assigned.', $claim->toArray()); + if ($claim->beamPack && Arr::get($this->data, 'is_pack') && ! $isPackUpdated) { + $claim->beamPack->update(['is_claimed' => true]); + $isPackUpdated = true; + } + } // Delete scan after claim is set up so the signed data can't be used to claim again. BeamScan::firstWhere(['wallet_public_key' => $data['wallet_public_key'], 'beam_id' => $data['beam']['id']])?->delete(); - DB::commit(); - - Log::info('ClaimBeamJob: Claim assigned.', $claim->toArray()); } else { Cache::put(BeamService::key(Arr::get($data, 'beam.code')), 0); Log::info('ClaimBeamJob: No claim available, setting remaining count to 0', $data); @@ -66,7 +75,7 @@ public function handle(BatchService $batch, WalletService $wallet): void } catch (Throwable $e) { DB::rollBack(); - Log::error('ClaimBeamJob: Claim error, message:' . $e->getMessage(), $data); + Log::error('ClaimBeamJob: Claim error, message: ' . $e->getMessage(), $data); throw $e; } finally { @@ -81,7 +90,7 @@ public function handle(BatchService $batch, WalletService $wallet): void public function failed(Throwable $exception): void { if ($data = $this->data) { - if ($this->claimQuery($data)->count() > 0) { + if (count($this->claims($data)) > 0) { // Idempotency key prevents incrementing cache on same claim request even with manual retry on horizon $key = Arr::get($data, 'idempotency_key'); if (! Cache::get(PlatformBeamCache::IDEMPOTENCY_KEY->key($key))) { @@ -99,12 +108,24 @@ public function failed(Throwable $exception): void /** * Get the claim query. */ - protected function claimQuery(array $data): Builder + protected function claims(array $data): Collection { return BeamClaim::where('beam_id', $data['beam']['id']) + ->with(['beam:id,collection_chain_id', 'beamPack:id,beam_id']) ->claimable() - ->when($data['code'], fn ($query) => $query->withSingleUseCode($data['code'])) - ->unless($data['code'], fn ($query) => $query->inRandomOrder()); + ->when($isPack = Arr::get($data, 'is_pack'), function (Builder $query) use ($data) { + if (!($pack = BeamPack::where('is_claimed', false) + ->where('beam_id', $data['beam']['id']) + ->when($data['code'], fn ($subquery) => $subquery->where('code', $data['code'])) + ->inRandomOrder() + ->first())) { + throw new BeamException('No available packs to claim.'); + } + $query->where('beam_pack_id', $pack->id); + }) + ->when(!$isPack && $data['code'], fn ($query) => $query->withSingleUseCode($data['code'])) + ->when(!$isPack, fn ($query) => $query->inRandomOrder()) + ->get(['id', 'beam_id', 'type', 'beam_pack_id']); } /** diff --git a/src/Services/BeamService.php b/src/Services/BeamService.php index 512fa94..1f187e3 100644 --- a/src/Services/BeamService.php +++ b/src/Services/BeamService.php @@ -235,14 +235,16 @@ public function findByCode(string $code): ?Model */ public function scanByCode(string $code, ?string $wallet = null): ?Model { - $isSingleUse = static::isSingleUse($code); + $beamCode = static::getSingleUseCodeData($code)?->beamCode; + $beam = Beam::whereCode($beamCode ?? $code)->firstOrFail(); + + if ($beamCode) { + ($beam->is_pack ? new BeamPack() : new BeamClaim()) + ->withSingleUseCode($code) + ->firstOrFail(); + } + - $beam = $isSingleUse - ? BeamClaim::withSingleUseCode($code) - ->with('beam') - ->first() - ->beam - : $this->findByCode($code); if ($wallet) { // Pushing this to the queue for performance CreateClaim::dispatch($claim = [ @@ -254,7 +256,7 @@ public function scanByCode(string $code, ?string $wallet = null): ?Model $beam->setRelation('scans', collect(json_decode(json_encode([$claim])))); } - if ($isSingleUse) { + if ($beamCode) { $beam['code'] = $code; } @@ -267,23 +269,22 @@ public function scanByCode(string $code, ?string $wallet = null): ?Model public function claim(string $code, string $wallet, ?string $idempotencyKey = null): bool { $singleUseCode = null; - $singleUse = static::isSingleUse($code); - - if ($singleUse) { - $singleUseCode = $code; - $beam = BeamClaim::withSingleUseCode($singleUseCode) - ->with('beam') - ->first() - ->beam; - $code = $beam?->code; - } else { - $beam = $this->findByCode($code); - } - + $singleUse = static::getSingleUseCodeData($code); + $beam = $this->findByCode($singleUse ? $singleUse->beamCode : $code); if (! $beam) { throw new BeamException(__('enjin-platform-beam::error.beam_not_found', ['code' => $code])); } + if ($singleUse) { + if (!($beam->is_pack ? new BeamPack() : new BeamClaim()) + ->withSingleUseCode($code) + ->first()) { + throw new BeamException(__('enjin-platform-beam::error.beam_not_found', ['code' => $code])); + } + $singleUseCode = $singleUse->claimCode; + $code = $singleUse->beamCode; + } + $lock = Cache::lock(self::key($code, 'claim-lock'), 5); try { @@ -614,6 +615,7 @@ protected function buildRequiredClaimBeamData( 'state' => ClaimStatus::PENDING->name, 'beam' => $beam->toArray(), 'beam_id' => $beam->id, + 'is_pack' => $beam->is_pack, 'ip_address' => request()->getClientIp(), 'code' => $singleUseCode, 'idempotency_key' => $idempotencyKey ?: Str::uuid()->toString(), diff --git a/tests/Feature/GraphQL/Mutations/ClaimBeamTest.php b/tests/Feature/GraphQL/Mutations/ClaimBeamTest.php index b49a2f5..4a1a9de 100644 --- a/tests/Feature/GraphQL/Mutations/ClaimBeamTest.php +++ b/tests/Feature/GraphQL/Mutations/ClaimBeamTest.php @@ -22,6 +22,7 @@ use Enjin\Platform\Support\BitMask; use Enjin\Platform\Support\SS58Address; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Queue; @@ -98,6 +99,20 @@ public function test_it_can_claim_beam_with_sr25519_single_use_codes(): void $this->genericClaimTest(CryptoSignatureType::SR25519, Arr::get($response, 'edges.0.node.code')); } + public function test_it_can_claim_beam_pack_with_sr25519_single_use_codes(): void + { + $code = $this->graphql('CreateBeam', $this->generateBeamPackData( + BeamType::MINT_ON_DEMAND, + 1, + [], + [['flag' => 'SINGLE_USE']], + )); + $response = $this->graphql('GetSingleUseCodes', ['code' => $code]); + $this->assertNotEmpty($response['totalCount']); + + $this->genericClaimTest(CryptoSignatureType::SR25519, Arr::get($response, 'edges.0.node.code')); + } + /** * Test claiming beam with ed25519. */ @@ -106,6 +121,18 @@ public function test_it_can_claim_beam_with_ed25519(): void $this->genericClaimTest(CryptoSignatureType::ED25519); } + public function test_it_can_claim_beam_pack_with_ed25519(): void + { + $this->seedBeamPack(); + $this->genericClaimTest(CryptoSignatureType::ED25519); + } + + public function test_it_can_claim_beam_pack_with_sr25519(): void + { + $this->seedBeamPack(); + $this->genericClaimTest(CryptoSignatureType::SR25519); + } + public function test_it_can_claim_beam_job_with_idempotency_key(): void { $data = [ @@ -488,7 +515,7 @@ protected function genericClaimTest(CryptoSignatureType $type = CryptoSignatureT ]); $this->assertNotEmpty($response['message']); if (! $singleUseCode) { - $this->assertEquals(1, $this->beam->scans()->count()); + $this->assertEquals(1, DB::table('beam_scans')->where('beam_id', $this->beam->id)->count()); } $message = $response['message']['message'];