• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marscoin / martianrepublic / 23825068715

31 Mar 2026 11:59PM UTC coverage: 11.001% (+0.6%) from 10.419%
23825068715

push

github

Martian Congress
feat: A2 service layer — IpfsService + ProposalService with tests

New services extracted from controllers:

IpfsService (app/Services/IpfsService.php):
  - pinFile(), pinContent(), pinFolder() — local IPFS node API
  - get() — retrieve via gateway
  - isValidCID(), gatewayUrl() — validation + URL generation
  - Used by ContentApiController (replaces AppHelper::upload)

ProposalService (app/Services/ProposalService.php):
  - syncPhases() — screening→voting→sunset transitions
  - tallyVotes() — count YES/NO with pass/fail threshold
  - getStats() — dashboard counts
  - Used by CongressController (replaces inline logic)

Tests (10 new, total 146):
  - CID validation, gateway URLs, pinFile null safety
  - Phase transitions, vote tallying (pass + fail), stats
  - BlockchainRpc config reading
  - All SQLite-compatible (no MySQL-only syntax in tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

42 of 112 new or added lines in 3 files covered. (37.5%)

1 existing line in 1 file now uncovered.

643 of 5845 relevant lines covered (11.0%)

1.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.04
/app/Services/ProposalService.php
1
<?php
2

3
namespace App\Services;
4

5
use App\Models\Proposals;
6
use Illuminate\Support\Facades\DB;
7
use Illuminate\Support\Facades\Log;
8

9
/**
10
 * Proposal lifecycle management — phase transitions, tallying, sunset.
11
 * Extracted from CongressController to enable reuse by scanner, cron, and tests.
12
 */
13
class ProposalService
14
{
15
    /**
16
     * Synchronize proposal phases based on timestamps.
17
     * Should be called on every Congress page load and by the scheduler.
18
     */
NEW
19
    public function syncPhases(): void
×
20
    {
NEW
21
        $now = now();
×
22

23
        // Screening → voting
NEW
24
        Proposals::where('status', 'screening')
×
NEW
25
            ->whereNotNull('screening_ends_at')
×
NEW
26
            ->where('screening_ends_at', '<=', $now)
×
NEW
27
            ->update(['status' => 'voting']);
×
28

29
        // Voting expired
NEW
30
        Proposals::where('status', 'voting')
×
NEW
31
            ->whereNotNull('voting_ends_at')
×
NEW
32
            ->where('voting_ends_at', '<=', $now)
×
NEW
33
            ->update(['active' => 0]);
×
34

35
        // Legacy expiration (pre-tier proposals)
NEW
36
        Proposals::where(DB::raw('DATE_ADD(mined, INTERVAL duration DAY)'), '<', $now)
×
NEW
37
            ->whereNull('voting_ends_at')
×
NEW
38
            ->update(['active' => 0]);
×
39

40
        // Active → sunset
NEW
41
        Proposals::where('status', 'active')
×
NEW
42
            ->whereNotNull('sunset_at')
×
NEW
43
            ->where('sunset_at', '<=', $now)
×
NEW
44
            ->update(['status' => 'sunset', 'active' => 0]);
×
45
    }
46

47
    /**
48
     * Tally votes for a proposal.
49
     * Returns [yays, nays, total, yay_percent, nay_percent, passed].
50
     */
51
    public function tallyVotes(int $proposalId): array
2✔
52
    {
53
        $yays = DB::table('votes')
2✔
54
            ->where('proposal_id', $proposalId)
2✔
55
            ->where('vote', 'YES')
2✔
56
            ->count();
2✔
57

58
        $nays = DB::table('votes')
2✔
59
            ->where('proposal_id', $proposalId)
2✔
60
            ->where('vote', 'NO')
2✔
61
            ->count();
2✔
62

63
        $total = $yays + $nays;
2✔
64
        $yayPct = $total > 0 ? round(100 * $yays / $total, 1) : 0;
2✔
65
        $nayPct = $total > 0 ? round(100 * $nays / $total, 1) : 0;
2✔
66

67
        return [
2✔
68
            'yays' => $yays,
2✔
69
            'nays' => $nays,
2✔
70
            'total' => $total,
2✔
71
            'yay_percent' => $yayPct,
2✔
72
            'nay_percent' => $nayPct,
2✔
73
            'passed' => $yayPct >= 65 && $total > 0,
2✔
74
        ];
2✔
75
    }
76

77
    /**
78
     * Get proposal stats for the dashboard.
79
     */
80
    public function getStats(): array
1✔
81
    {
82
        return [
1✔
83
            'total' => Proposals::count(),
1✔
84
            'active' => Proposals::where('active', 1)->count(),
1✔
85
            'passed' => Proposals::where('status', 'passed')->count(),
1✔
86
            'rejected' => Proposals::where('status', 'rejected')->count(),
1✔
87
            'citizens' => DB::table('feed')->where('tag', 'CT')->distinct('userid')->count('userid'),
1✔
88
            'general_public' => DB::table('feed')->where('tag', 'GP')->distinct('userid')->count('userid'),
1✔
89
        ];
1✔
90
    }
91
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc