File size: 3,151 Bytes
45dc401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?php

declare(strict_types=1);

namespace App\Domain\Analytics;

use App\Infrastructure\Database\BaseRepository;
use App\Infrastructure\Ids\UuidGenerator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

final class AnalyticsSessionRepository extends BaseRepository
{
    public function __construct(private readonly UuidGenerator $ids) {}

    protected function table(): string
    {
        return 'analytics_sessions';
    }

    protected function hasDeletedAtColumn(): bool
    {
        return false;
    }

    public function findById(string $id): ?\stdClass
    {
        return DB::table($this->table())->where('id', $id)->first();
    }

    /**
     * Open a new session. Returns the new session id.
     * $data must include visitor_id, entry_path, started_at, and any geo/UA/UTM fields.
     */
    public function open(array $data): string
    {
        $id  = $this->ids->generate();
        $now = $data['started_at'] ?? Carbon::now()->toDateTimeString();

        DB::table($this->table())->insert(array_merge($data, [
            'id'         => $id,
            'status'     => 'open',
            'page_count' => 0,
            'is_bot'     => $data['is_bot'] ?? 0,
            'created_at' => $now,
            'updated_at' => $now,
        ]));
        return $id;
    }

    /**
     * Called only by AnalyticsSessionTransitioner after validating the open→closed edge.
     */
    public function close(string $id, string $exitPath, int $durationSeconds): void
    {
        $now = Carbon::now()->toDateTimeString();
        DB::table($this->table())->where('id', $id)->update([
            'status'           => 'closed',
            'ended_at'         => $now,
            'exit_path'        => $exitPath,
            'duration_seconds' => $durationSeconds,
            'updated_at'       => $now,
        ]);
    }

    public function incrementPageCount(string $id): void
    {
        DB::table($this->table())->where('id', $id)->increment('page_count', 1, [
            'updated_at' => Carbon::now()->toDateTimeString(),
        ]);
    }

    public function updateExitPath(string $id, string $path): void
    {
        DB::table($this->table())->where('id', $id)->update([
            'exit_path'  => $path,
            'updated_at' => Carbon::now()->toDateTimeString(),
        ]);
    }

    /**
     * Open a session using the client-supplied UUID as the row id.
     * Idempotent: silently no-ops if that session_id already exists
     * (handles duplicate beacon delivery or concurrent requests).
     */
    public function openIdempotent(string $sessionId, array $data): void
    {
        $exists = DB::table($this->table())->where('id', $sessionId)->exists();
        if ($exists) {
            return;
        }

        $now = Carbon::now()->toDateTimeString();
        DB::table($this->table())->insert(array_merge($data, [
            'id'         => $sessionId,
            'status'     => 'open',
            'page_count' => 0,
            'is_bot'     => 0,
            'started_at' => $now,
            'created_at' => $now,
            'updated_at' => $now,
        ]));
    }
}