Public Access
feat(points): show early finish and expected points on questions
Core pays 10 early points for an unproven Steam finish. The rest comes with proof. Show this in the toast and the game line. Questions cards show the full value from GET /api/rewards/expected.
This commit is contained in:
1 parent
814decc781
commit
c126592887
13 files changed
+166
-28
No files matched your search
@@ -56,7 +56,10 @@ To Be Played is split into three repositories:
|
|||||||
Play Next "Start", Questions, add game) `usePointsCheck` asks Core `GET /api/rewards/games/{appid}`, reloads the wallet
|
Play Next "Start", Questions, add game) `usePointsCheck` asks Core `GET /api/rewards/games/{appid}`, reloads the wallet
|
||||||
when the balance moved and shows a toast ("+N points", "Already paid on ...", "No points yet: reason"). The game page
|
when the balance moved and shows a toast ("+N points", "Already paid on ...", "No points yet: reason"). The game page
|
||||||
shows the paid rows and the reason per missing kind (`GamePointsLine`); `pointsNotice` in `utils/rewards.js` picks the
|
shows the paid rows and the reason per missing kind (`GamePointsLine`); `pointsNotice` in `utils/rewards.js` picks the
|
||||||
toast, the reason codes come from Core and `POINTS.REASONS` only maps them to texts.
|
toast, the reason codes come from Core and `POINTS.REASONS` only maps them to texts. A finish without proof pays 10
|
||||||
|
early points (toast "+10 points; the rest comes with proof", game line "The rest of the finish points: play once on
|
||||||
|
Steam after finishing"), and the rest once Steam play proves it. A "Finished?" card on the Questions page shows
|
||||||
|
"Up to N points" from `GET /api/rewards/expected`; when that call fails the card shows no line.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
@@ -65,6 +68,7 @@ flowchart LR
|
|||||||
Check --> Toast["Toast"]
|
Check --> Toast["Toast"]
|
||||||
Check -->|balance moved| Wallet["reloadWallet: header"]
|
Check -->|balance moved| Wallet["reloadWallet: header"]
|
||||||
Core --> Line["GamePointsLine on the game page"]
|
Core --> Line["GamePointsLine on the game page"]
|
||||||
|
Questions["Questions page"] -->|"GET /api/rewards/expected"| CoreExpected["Core /api/rewards/expected"]
|
||||||
```
|
```
|
||||||
- **Landing page**: next to the community numbers and the top games, three blocks: games per platform, top genres and
|
- **Landing page**: next to the community numbers and the top games, three blocks: games per platform, top genres and
|
||||||
finished games per year of all users.
|
finished games per year of all users.
|
||||||
|
|||||||
+14
-7
@@ -1180,15 +1180,15 @@
|
|||||||
"HOW_TO": "So bekommst du Punkte",
|
"HOW_TO": "So bekommst du Punkte",
|
||||||
"RULES": {
|
"RULES": {
|
||||||
"STARTED": "5 Punkte, wenn du ein Spiel von deinem Stapel startest und spielst (Steam-Spiele, einmal pro Spiel).",
|
"STARTED": "5 Punkte, wenn du ein Spiel von deinem Stapel startest und spielst (Steam-Spiele, einmal pro Spiel).",
|
||||||
"FINISHED": "30, 50 oder 80 Punkte, wenn du ein Spiel durchspielst (nach seiner Spielzeit), sofern du es wirklich gespielt hast. Einmal pro Spiel.",
|
"FINISHED": "30, 50 oder 80 Punkte, wenn du ein Spiel durchspielst (nach seiner Spielzeit) und es danach gespielt hast. Ohne Beweis für Spielen bekommst du jetzt 10 Punkte und den Rest, sobald du es nach dem Durchspielen auf Steam spielst. Bei 0 Spielzeit bekommst du 10, den Rest nur mit Steam-Spielzeit. Einmal pro Spiel.",
|
||||||
"COMPLETED": "+40 Punkte, wenn du in einem abgeschlossenen Spiel 100 % der Erfolge hast. Es braucht ein belegtes Steam-Durchspielen; ein manuelles öffnet es nie.",
|
"COMPLETED": "+40 Punkte, wenn du in einem abgeschlossenen Spiel 100 % der Erfolge hast. Es braucht ein belegtes Steam-Durchspielen (die frühen 10 Punkte zählen erst mit Beweis); ein manuelles öffnet es nie.",
|
||||||
"FINISHED_MANUAL": "15 Punkte für ein manuelles Spiel, das du durchspielst, höchstens 3 pro Woche (Montag bis Sonntag). Ein 4. Spiel in dieser Woche zahlt nichts.",
|
"FINISHED_MANUAL": "15 Punkte für ein manuelles Spiel, das du durchspielst. Höchstens 3 Spiele pro Woche (Montag bis Sonntag) zahlen Punkte; manuelle Spiele und Steam-Spiele ohne Beweis teilen sich dieses Limit. Ein 4. manuelles Durchspielen in dieser Woche zahlt nichts; ein 4. Steam-Durchspielen ohne Beweis zahlt mit Beweis weiter voll.",
|
||||||
"MONTH": "40 Punkte, wenn dein Stapel am Monatsende kleiner ist als am Monatsanfang und du in dem Monat ein Spiel durchgespielt hast.",
|
"MONTH": "40 Punkte, wenn dein Stapel am Monatsende kleiner ist als am Monatsanfang und du in dem Monat ein Spiel durchgespielt hast.",
|
||||||
"STREAK": "20 Punkte für jede Woche mit einem beendeten Spiel nach einer Woche mit einem beendeten Spiel.",
|
"STREAK": "20 Punkte für jede Woche mit einem beendeten Spiel nach einer Woche mit einem beendeten Spiel.",
|
||||||
"UNLOCK": "Gegenstände mit Schloss kann man nicht kaufen. Sie werden einmal und für immer freigeschaltet, wenn du ihr Ziel erreichst.",
|
"UNLOCK": "Gegenstände mit Schloss kann man nicht kaufen. Sie werden einmal und für immer freigeschaltet, wenn du ihr Ziel erreichst.",
|
||||||
"PROOF": "Ein Steam-Spiel braucht als Beweis frisches Spielen: den Abschluss-Erfolg (höchstens 30 Tage vor dem Durchspielen freigeschaltet), Spielzeit ab 30 Tage vor dem Durchspielen oder die halbe Normalzeit danach gespielt.",
|
"PROOF": "Ein Steam-Spiel braucht als Beweis frisches Spielen: den Abschluss-Erfolg (höchstens 30 Tage vor dem Durchspielen freigeschaltet), Spielzeit ab 30 Tage vor dem Durchspielen oder die halbe Normalzeit danach gespielt.",
|
||||||
"ONCE": "Jede Art zahlt einmal pro Spiel. Editionen eines Spiels zählen als eines.",
|
"ONCE": "Jede Art zahlt einmal pro Spiel. Editionen eines Spiels zählen als eines.",
|
||||||
"PLAYSTATION": "PlayStation-Spielzeit zahlt nichts und ist kein Beweis. Ein Spiel, das mit einem PlayStation-Titel verknüpft ist, zahlt vorerst nichts.",
|
"PLAYSTATION": "PlayStation-Spiele zahlen nichts und PlayStation-Spielzeit ist kein Beweis. Ein manuelles oder Steam-Spiel, das mit einem PlayStation-Titel verknüpft ist, zahlt nach seinen eigenen Regeln.",
|
||||||
"HIDDEN": "Versteckte Spiele zahlen nichts.",
|
"HIDDEN": "Versteckte Spiele zahlen nichts.",
|
||||||
"CHECKED": "Punkte werden geprüft, wenn du einen Status änderst, ein Spiel oder die App öffnest, und jede Nacht.",
|
"CHECKED": "Punkte werden geprüft, wenn du einen Status änderst, ein Spiel oder die App öffnest, und jede Nacht.",
|
||||||
"STARTER": "Durchgespielte Spiele aus der Zeit vor dem Status-Verlauf bekamen einmal Startpunkte (20 pro Spiel, höchstens 250)."
|
"STARTER": "Durchgespielte Spiele aus der Zeit vor dem Status-Verlauf bekamen einmal Startpunkte (20 pro Spiel, höchstens 250)."
|
||||||
@@ -1198,23 +1198,27 @@
|
|||||||
"OTHER_EDITION": "(andere Edition des Spiels)",
|
"OTHER_EDITION": "(andere Edition des Spiels)",
|
||||||
"WAITING": "Durchgespielt: wartet auf Beweis. Spiele das Spiel nach dem Durchspielen einmal.",
|
"WAITING": "Durchgespielt: wartet auf Beweis. Spiele das Spiel nach dem Durchspielen einmal.",
|
||||||
"WAITING_START": "Gestartet: spiele das Spiel einmal, um die 5 Punkte zu bekommen.",
|
"WAITING_START": "Gestartet: spiele das Spiel einmal, um die 5 Punkte zu bekommen.",
|
||||||
|
"WAITING_TOPUP": "Der Rest der Durchspiel-Punkte: spiele nach dem Durchspielen einmal auf Steam.",
|
||||||
"NONE": "{{kind}}: keine Punkte - {{reason}}",
|
"NONE": "{{kind}}: keine Punkte - {{reason}}",
|
||||||
"TOAST_BOOKED": "+{{count}} Punkte",
|
"TOAST_BOOKED": "+{{count}} Punkte",
|
||||||
|
"TOAST_BOOKED_REST": "+{{count}} Punkte; den Rest gibt es mit Beweis",
|
||||||
"TOAST_PAID": "Schon bezahlt am {{date}}",
|
"TOAST_PAID": "Schon bezahlt am {{date}}",
|
||||||
|
"TOAST_PAID_REST": "Schon bezahlt am {{date}}; den Rest gibt es mit Beweis",
|
||||||
"TOAST_NONE": "Noch keine Punkte: {{reason}}",
|
"TOAST_NONE": "Noch keine Punkte: {{reason}}",
|
||||||
"TOAST_STATUS": "Dieser Status bringt keine Punkte"
|
"TOAST_STATUS": "Dieser Status bringt keine Punkte"
|
||||||
},
|
},
|
||||||
"REASONS": {
|
"REASONS": {
|
||||||
"HIDDEN": "das Spiel ist ausgeblendet",
|
"HIDDEN": "das Spiel ist ausgeblendet",
|
||||||
"PLAYSTATION": "PlayStation-Spiele zahlen nichts",
|
"PLAYSTATION": "PlayStation-Spiele zahlen nichts",
|
||||||
"PLAYSTATION_LINKED": "das Spiel ist mit einem PlayStation-Titel verknüpft (zahlt vorerst nichts)",
|
|
||||||
"BEFORE_HISTORY": "durchgespielt, bevor der Status-Verlauf begann (siehe Startpunkte)",
|
"BEFORE_HISTORY": "durchgespielt, bevor der Status-Verlauf begann (siehe Startpunkte)",
|
||||||
"MANUAL_GAME": "ein manuelles Spiel hat keine Spielzeit als Beweis",
|
"MANUAL_GAME": "ein manuelles Spiel hat keine Spielzeit als Beweis",
|
||||||
"NO_PLAY_AFTER_START": "seit dem Start nicht gespielt",
|
"NO_PLAY_AFTER_START": "seit dem Start nicht gespielt",
|
||||||
"NO_PLAYTIME": "keine Spielzeit erfasst",
|
"NO_PLAYTIME": "keine Spielzeit erfasst",
|
||||||
|
"PLAY_NOT_COUNTED": "PlayStation-Minuten sind in dieses Spiel eingerechnet, darum ist Spielen kein Beweis",
|
||||||
"NO_PROOF": "kein Beweis für frisches Spielen, spiele einmal nach dem Durchspielen",
|
"NO_PROOF": "kein Beweis für frisches Spielen, spiele einmal nach dem Durchspielen",
|
||||||
"WEEKLY_LIMIT": "Wochenlimit erreicht",
|
"WEEKLY_LIMIT": "Wochenlimit erreicht",
|
||||||
"NEEDS_PROVEN_FINISH": "braucht zuerst ein bewiesenes Durchspielen",
|
"WEEKLY_LIMIT_NO_PROOF": "Wochenlimit erreicht, also keine frühen Punkte; ein Beweis zahlt weiter das volle Durchspielen",
|
||||||
|
"NEEDS_PROVEN_FINISH": "braucht zuerst einen Beweis für Spielen auf Steam",
|
||||||
"NOT_ALL_ACHIEVEMENTS": "nicht alle Erfolge freigeschaltet"
|
"NOT_ALL_ACHIEVEMENTS": "nicht alle Erfolge freigeschaltet"
|
||||||
},
|
},
|
||||||
"SLOTS": {
|
"SLOTS": {
|
||||||
@@ -1290,6 +1294,8 @@
|
|||||||
"STARTED": "Gestartet",
|
"STARTED": "Gestartet",
|
||||||
"FINISHED": "Durchgespielt",
|
"FINISHED": "Durchgespielt",
|
||||||
"FINISHED_MANUAL": "Durchgespielt (manuelles Spiel)",
|
"FINISHED_MANUAL": "Durchgespielt (manuelles Spiel)",
|
||||||
|
"FINISHED_EARLY": "Durchgespielt (frühe Punkte)",
|
||||||
|
"FINISHED_TOPUP": "Durchspielen bewiesen, Rest",
|
||||||
"COMPLETED": "100-%-Bonus",
|
"COMPLETED": "100-%-Bonus",
|
||||||
"MONTH": "Kleinerer Stapel im",
|
"MONTH": "Kleinerer Stapel im",
|
||||||
"STREAK": "Serien-Woche",
|
"STREAK": "Serien-Woche",
|
||||||
@@ -1350,7 +1356,8 @@
|
|||||||
"FINISHED": {
|
"FINISHED": {
|
||||||
"ASK": "Durchgespielt?",
|
"ASK": "Durchgespielt?",
|
||||||
"REASON": "Du hast {{playtime}} gespielt.",
|
"REASON": "Du hast {{playtime}} gespielt.",
|
||||||
"REASON_TTB": "Du hast {{playtime}} gespielt. Die meisten brauchen {{ttb}}."
|
"REASON_TTB": "Du hast {{playtime}} gespielt. Die meisten brauchen {{ttb}}.",
|
||||||
|
"POINTS": "Bis zu {{count}} Punkte"
|
||||||
},
|
},
|
||||||
"DROPPED": {
|
"DROPPED": {
|
||||||
"ASK": "Abgebrochen?",
|
"ASK": "Abgebrochen?",
|
||||||
|
|||||||
+14
-7
@@ -1180,15 +1180,15 @@
|
|||||||
"HOW_TO": "How to earn points",
|
"HOW_TO": "How to earn points",
|
||||||
"RULES": {
|
"RULES": {
|
||||||
"STARTED": "5 points when you start a game from your pile and play it (Steam games, once per game).",
|
"STARTED": "5 points when you start a game from your pile and play it (Steam games, once per game).",
|
||||||
"FINISHED": "30, 50 or 80 points when you finish a game (by its time to beat), if you really played it. Once per game.",
|
"FINISHED": "30, 50 or 80 points when you finish a game (by its time to beat) and played it since. Without proof of play you get 10 points now, and the rest once you play it on Steam after finishing. With 0 play time on record you get 10, and the rest only with Steam play. Once per game.",
|
||||||
"COMPLETED": "+40 points when you reach 100 % achievements in a completed game. It needs a proven Steam finish; a manual finish never opens it.",
|
"COMPLETED": "+40 points when you reach 100 % achievements in a completed game. It needs a proven Steam finish (the early 10 points do not count until proof arrives); a manual finish never opens it.",
|
||||||
"FINISHED_MANUAL": "15 points for a manual game you finish, at most 3 per week (Monday to Sunday). A 4th finish in that week pays nothing.",
|
"FINISHED_MANUAL": "15 points for a manual game you finish. At most 3 finishes per week (Monday to Sunday) pay points; manual finishes and Steam finishes without proof share this limit. A 4th manual finish in that week pays nothing; a 4th Steam finish without proof still pays in full once proven.",
|
||||||
"MONTH": "40 points when your pile is smaller at the end of a month than at its start and you finished a game that month.",
|
"MONTH": "40 points when your pile is smaller at the end of a month than at its start and you finished a game that month.",
|
||||||
"STREAK": "20 points for every week with a finished game after a week with a finished game.",
|
"STREAK": "20 points for every week with a finished game after a week with a finished game.",
|
||||||
"UNLOCK": "Items with a lock cannot be bought. They unlock once, for good, when you reach their goal.",
|
"UNLOCK": "Items with a lock cannot be bought. They unlock once, for good, when you reach their goal.",
|
||||||
"PROOF": "A Steam finish needs proof of fresh play: the ending achievement (not older than 30 days before the finish), any play from 30 days before the finish on, or half of the normal time played after it.",
|
"PROOF": "A Steam finish needs proof of fresh play: the ending achievement (not older than 30 days before the finish), any play from 30 days before the finish on, or half of the normal time played after it.",
|
||||||
"ONCE": "Each kind pays once per game. Editions of one game count as one.",
|
"ONCE": "Each kind pays once per game. Editions of one game count as one.",
|
||||||
"PLAYSTATION": "PlayStation play pays nothing and is no proof. A game linked to a PlayStation title pays nothing for now.",
|
"PLAYSTATION": "PlayStation games pay nothing and PlayStation play is no proof. A manual or Steam game linked to a PlayStation title pays by its own rules.",
|
||||||
"HIDDEN": "Hidden games pay nothing.",
|
"HIDDEN": "Hidden games pay nothing.",
|
||||||
"CHECKED": "Points are checked when you change a status, open a game or the app, and every night.",
|
"CHECKED": "Points are checked when you change a status, open a game or the app, and every night.",
|
||||||
"STARTER": "Finishes from before the status history got starter points once (20 per game, 250 at most)."
|
"STARTER": "Finishes from before the status history got starter points once (20 per game, 250 at most)."
|
||||||
@@ -1198,23 +1198,27 @@
|
|||||||
"OTHER_EDITION": "(other edition of the game)",
|
"OTHER_EDITION": "(other edition of the game)",
|
||||||
"WAITING": "Finished: waiting for proof. Play the game once after finishing.",
|
"WAITING": "Finished: waiting for proof. Play the game once after finishing.",
|
||||||
"WAITING_START": "Started: play the game once to get the 5 points.",
|
"WAITING_START": "Started: play the game once to get the 5 points.",
|
||||||
|
"WAITING_TOPUP": "The rest of the finish points: play once on Steam after finishing.",
|
||||||
"NONE": "{{kind}}: no points - {{reason}}",
|
"NONE": "{{kind}}: no points - {{reason}}",
|
||||||
"TOAST_BOOKED": "+{{count}} points",
|
"TOAST_BOOKED": "+{{count}} points",
|
||||||
|
"TOAST_BOOKED_REST": "+{{count}} points; the rest comes with proof",
|
||||||
"TOAST_PAID": "Already paid on {{date}}",
|
"TOAST_PAID": "Already paid on {{date}}",
|
||||||
|
"TOAST_PAID_REST": "Already paid on {{date}}; the rest comes with proof",
|
||||||
"TOAST_NONE": "No points yet: {{reason}}",
|
"TOAST_NONE": "No points yet: {{reason}}",
|
||||||
"TOAST_STATUS": "This status earns no points"
|
"TOAST_STATUS": "This status earns no points"
|
||||||
},
|
},
|
||||||
"REASONS": {
|
"REASONS": {
|
||||||
"HIDDEN": "the game is hidden",
|
"HIDDEN": "the game is hidden",
|
||||||
"PLAYSTATION": "PlayStation games pay nothing",
|
"PLAYSTATION": "PlayStation games pay nothing",
|
||||||
"PLAYSTATION_LINKED": "the game is linked to a PlayStation title (pays nothing for now)",
|
|
||||||
"BEFORE_HISTORY": "finished before the status history began (see starter points)",
|
"BEFORE_HISTORY": "finished before the status history began (see starter points)",
|
||||||
"MANUAL_GAME": "a manual game has no play time to prove it",
|
"MANUAL_GAME": "a manual game has no play time to prove it",
|
||||||
"NO_PLAY_AFTER_START": "no play since the start",
|
"NO_PLAY_AFTER_START": "no play since the start",
|
||||||
"NO_PLAYTIME": "no play time on record",
|
"NO_PLAYTIME": "no play time on record",
|
||||||
|
"PLAY_NOT_COUNTED": "PlayStation minutes are mixed into this game, so play on it is no proof",
|
||||||
"NO_PROOF": "no proof of fresh play, play once after finishing",
|
"NO_PROOF": "no proof of fresh play, play once after finishing",
|
||||||
"WEEKLY_LIMIT": "weekly limit reached",
|
"WEEKLY_LIMIT": "weekly limit reached",
|
||||||
"NEEDS_PROVEN_FINISH": "needs a proven finish first",
|
"WEEKLY_LIMIT_NO_PROOF": "weekly limit reached, so no early points; proof still pays the full finish",
|
||||||
|
"NEEDS_PROVEN_FINISH": "needs proof of play on Steam first",
|
||||||
"NOT_ALL_ACHIEVEMENTS": "not all achievements unlocked"
|
"NOT_ALL_ACHIEVEMENTS": "not all achievements unlocked"
|
||||||
},
|
},
|
||||||
"SLOTS": {
|
"SLOTS": {
|
||||||
@@ -1290,6 +1294,8 @@
|
|||||||
"STARTED": "Started",
|
"STARTED": "Started",
|
||||||
"FINISHED": "Finished",
|
"FINISHED": "Finished",
|
||||||
"FINISHED_MANUAL": "Finished (manual game)",
|
"FINISHED_MANUAL": "Finished (manual game)",
|
||||||
|
"FINISHED_EARLY": "Finished (early points)",
|
||||||
|
"FINISHED_TOPUP": "Finish proven, rest",
|
||||||
"COMPLETED": "100 % bonus",
|
"COMPLETED": "100 % bonus",
|
||||||
"MONTH": "Smaller pile in",
|
"MONTH": "Smaller pile in",
|
||||||
"STREAK": "Streak week",
|
"STREAK": "Streak week",
|
||||||
@@ -1350,7 +1356,8 @@
|
|||||||
"FINISHED": {
|
"FINISHED": {
|
||||||
"ASK": "Finished?",
|
"ASK": "Finished?",
|
||||||
"REASON": "You played {{playtime}}.",
|
"REASON": "You played {{playtime}}.",
|
||||||
"REASON_TTB": "You played {{playtime}}. Most players need {{ttb}}."
|
"REASON_TTB": "You played {{playtime}}. Most players need {{ttb}}.",
|
||||||
|
"POINTS": "Up to {{count}} points"
|
||||||
},
|
},
|
||||||
"DROPPED": {
|
"DROPPED": {
|
||||||
"ASK": "Dropped?",
|
"ASK": "Dropped?",
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import {formatDate} from '../utils/format.js';
|
|||||||
import {STATUS_NOT_REACHED} from '../utils/rewards.js';
|
import {STATUS_NOT_REACHED} from '../utils/rewards.js';
|
||||||
|
|
||||||
const WAITING = {NO_PROOF: 'POINTS.GAME.WAITING', NO_PLAY_AFTER_START: 'POINTS.GAME.WAITING_START'};
|
const WAITING = {NO_PROOF: 'POINTS.GAME.WAITING', NO_PLAY_AFTER_START: 'POINTS.GAME.WAITING_START'};
|
||||||
|
// The rest of an early finish waits for Steam play, with or without playtime on record.
|
||||||
|
const WAITING_TOPUP = ['NO_PROOF', 'NO_PLAYTIME'];
|
||||||
|
|
||||||
|
const waitingKey = (kind, reason) => (kind === 'FINISHED_TOPUP' ? WAITING_TOPUP.includes(reason) && 'POINTS.GAME.WAITING_TOPUP' : WAITING[reason]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What the game earned: one line per paid row and one per kind that is missing, with the reason from Core. Loading it
|
* What the game earned: one line per paid row and one per kind that is missing, with the reason from Core. Loading it
|
||||||
@@ -36,7 +40,7 @@ export default function GamePointsLine({game}) {
|
|||||||
date: formatDate(row.paidAt, i18n.language, {dateStyle: 'medium'}),
|
date: formatDate(row.paidAt, i18n.language, {dateStyle: 'medium'}),
|
||||||
}) + (row.appid === game.appid ? '' : ` ${t('POINTS.GAME.OTHER_EDITION')}`)),
|
}) + (row.appid === game.appid ? '' : ` ${t('POINTS.GAME.OTHER_EDITION')}`)),
|
||||||
...Object.entries(points.missing).filter(([, reason]) => !STATUS_NOT_REACHED.includes(reason)).map(([kind, reason]) => (
|
...Object.entries(points.missing).filter(([, reason]) => !STATUS_NOT_REACHED.includes(reason)).map(([kind, reason]) => (
|
||||||
WAITING[reason] ? t(WAITING[reason])
|
waitingKey(kind, reason) ? t(waitingKey(kind, reason))
|
||||||
: t('POINTS.GAME.NONE', {kind: kindText(kind), reason: t(`POINTS.REASONS.${reason}`)}))),
|
: t('POINTS.GAME.NONE', {kind: kindText(kind), reason: t(`POINTS.REASONS.${reason}`)}))),
|
||||||
];
|
];
|
||||||
return lines.map((line, i) => <p key={i} className="text-sm text-muted-foreground">{line}</p>);
|
return lines.map((line, i) => <p key={i} className="text-sm text-muted-foreground">{line}</p>);
|
||||||
|
|||||||
@@ -50,6 +50,32 @@ describe('GamePointsLine', () => {
|
|||||||
expect(screen.getAllByText(/^POINTS.GAME.NONE/)).toHaveLength(1);
|
expect(screen.getAllByText(/^POINTS.GAME.NONE/)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the early row and the waiting line for the rest of the finish', async () => {
|
||||||
|
renderLine('FINISHED', {balance: 10, paid: [row('FINISHED_EARLY', {points: 10})], missing: {FINISHED_TOPUP: 'NO_PROOF'}});
|
||||||
|
|
||||||
|
expect(await screen.findByText('POINTS.GAME.WAITING_TOPUP')).toBeTruthy();
|
||||||
|
expect(screen.getAllByText(/^POINTS.GAME.PAID/)).toHaveLength(1);
|
||||||
|
expect(screen.queryByText(/^POINTS.GAME.NONE/)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the waiting line for the rest when there is no play time on record', async () => {
|
||||||
|
renderLine('FINISHED', {balance: 10, paid: [row('FINISHED_EARLY', {points: 10})], missing: {FINISHED_TOPUP: 'NO_PLAYTIME'}});
|
||||||
|
|
||||||
|
expect(await screen.findByText('POINTS.GAME.WAITING_TOPUP')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a reason line for a hidden game that waits for the rest', async () => {
|
||||||
|
renderLine('FINISHED', {balance: 10, paid: [row('FINISHED_EARLY', {points: 10})], missing: {FINISHED_TOPUP: 'HIDDEN'}});
|
||||||
|
|
||||||
|
expect(await screen.findByText(/^POINTS.GAME.NONE/)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the top-up row with its date', async () => {
|
||||||
|
renderLine('FINISHED', {balance: 30, paid: [row('FINISHED_EARLY', {points: 10}), row('FINISHED_TOPUP', {points: 20})], missing: {}});
|
||||||
|
|
||||||
|
expect(await screen.findAllByText(/^POINTS.GAME.PAID/)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows no line for a status the game never reached', async () => {
|
it('shows no line for a status the game never reached', async () => {
|
||||||
renderLine('BACKLOG', {balance: 30, paid: [], missing: {STARTED: 'NOT_STARTED', FINISHED: 'NOT_FINISHED', COMPLETED: 'NOT_COMPLETED'}});
|
renderLine('BACKLOG', {balance: 30, paid: [], missing: {STARTED: 'NOT_STARTED', FINISHED: 'NOT_FINISHED', COMPLETED: 'NOT_COMPLETED'}});
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import {formatPlaytime} from '../utils/games.js';
|
|||||||
*
|
*
|
||||||
* @param item a result of `GET /api/library/questions`
|
* @param item a result of `GET /api/library/questions`
|
||||||
* @param busy true while an answer is on its way
|
* @param busy true while an answer is on its way
|
||||||
|
* @param expectedPoints the points a finish would pay (`GET /api/rewards/expected`), absent or 0 shows nothing
|
||||||
* @param onAnswer `({yes, rating?})`; a yes to the rating question comes with the stars
|
* @param onAnswer `({yes, rating?})`; a yes to the rating question comes with the stars
|
||||||
*/
|
*/
|
||||||
export default function QuestionCard({item, busy, onAnswer}) {
|
export default function QuestionCard({item, busy, expectedPoints, onAnswer}) {
|
||||||
const {t, i18n} = useTranslation();
|
const {t, i18n} = useTranslation();
|
||||||
const {appid, name, imageUrl, question, playtimeMinutes, normallyMinutes, lastActivity} = item;
|
const {appid, name, imageUrl, question, playtimeMinutes, normallyMinutes, lastActivity} = item;
|
||||||
const reasonKey = question === 'FINISHED' && normallyMinutes > 0 ? 'FINISHED.REASON_TTB' : `${question}.REASON`;
|
const reasonKey = question === 'FINISHED' && normallyMinutes > 0 ? 'FINISHED.REASON_TTB' : `${question}.REASON`;
|
||||||
@@ -38,6 +39,9 @@ export default function QuestionCard({item, busy, onAnswer}) {
|
|||||||
</Link>
|
</Link>
|
||||||
<p id={askId} className="text-base font-semibold">{t(`QUESTIONS.${question}.ASK`)}</p>
|
<p id={askId} className="text-base font-semibold">{t(`QUESTIONS.${question}.ASK`)}</p>
|
||||||
<p className="text-sm text-muted-foreground">{reason}</p>
|
<p className="text-sm text-muted-foreground">{reason}</p>
|
||||||
|
{question === 'FINISHED' && expectedPoints > 0 && (
|
||||||
|
<p className="text-sm font-medium text-primary">{t('QUESTIONS.FINISHED.POINTS', {count: expectedPoints})}</p>
|
||||||
|
)}
|
||||||
{question === 'RATE' ? (
|
{question === 'RATE' ? (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<RatingStars value={0} labelledBy={askId} disabled={busy}
|
<RatingStars value={0} labelledBy={askId} disabled={busy}
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ export default function usePointsCheck() {
|
|||||||
if (tell) {
|
if (tell) {
|
||||||
const notice = pointsNotice(data, status);
|
const notice = pointsNotice(data, status);
|
||||||
if (notice.type === 'booked') {
|
if (notice.type === 'booked') {
|
||||||
toast.success(t('POINTS.GAME.TOAST_BOOKED', {count: notice.points}));
|
toast.success(t(notice.rest ? 'POINTS.GAME.TOAST_BOOKED_REST' : 'POINTS.GAME.TOAST_BOOKED', {count: notice.points}));
|
||||||
} else if (notice.type === 'paid') {
|
} else if (notice.type === 'paid') {
|
||||||
toast(t('POINTS.GAME.TOAST_PAID', {date: formatDate(notice.date, i18n.language, {dateStyle: 'medium'})}));
|
toast(t(notice.rest ? 'POINTS.GAME.TOAST_PAID_REST' : 'POINTS.GAME.TOAST_PAID',
|
||||||
|
{date: formatDate(notice.date, i18n.language, {dateStyle: 'medium'})}));
|
||||||
} else if (notice.type === 'none') {
|
} else if (notice.type === 'none') {
|
||||||
toast(t('POINTS.GAME.TOAST_NONE', {reason: t(`POINTS.REASONS.${notice.reason}`)}));
|
toast(t('POINTS.GAME.TOAST_NONE', {reason: t(`POINTS.REASONS.${notice.reason}`)}));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import useInView from '../hooks/useInView.js';
|
|||||||
import {formatDate} from '../utils/format.js';
|
import {formatDate} from '../utils/format.js';
|
||||||
import {buyItem, equipItem, LOOKS, SLOTS} from '../utils/rewards.js';
|
import {buyItem, equipItem, LOOKS, SLOTS} from '../utils/rewards.js';
|
||||||
|
|
||||||
const GAME_KINDS = ['STARTED', 'FINISHED', 'FINISHED_MANUAL', 'COMPLETED'];
|
const GAME_KINDS = ['STARTED', 'FINISHED', 'FINISHED_MANUAL', 'FINISHED_EARLY', 'FINISHED_TOPUP', 'COMPLETED'];
|
||||||
const HOW_TO = ['STARTED', 'FINISHED', 'PROOF', 'COMPLETED', 'FINISHED_MANUAL', 'ONCE', 'PLAYSTATION', 'HIDDEN', 'CHECKED', 'MONTH',
|
const HOW_TO = ['STARTED', 'FINISHED', 'PROOF', 'COMPLETED', 'FINISHED_MANUAL', 'ONCE', 'PLAYSTATION', 'HIDDEN', 'CHECKED', 'MONTH',
|
||||||
'STREAK', 'STARTER', 'UNLOCK'];
|
'STREAK', 'STARTER', 'UNLOCK'];
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,17 @@ describe('PointsPage', () => {
|
|||||||
expect(screen.getByText('+50')).toBeTruthy();
|
expect(screen.getByText('+50')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('links early and top-up rows to the game', async () => {
|
||||||
|
await renderPage({'GET /api/rewards/wallet': json(200, wallet({history: [
|
||||||
|
{kind: 'FINISHED_EARLY', ref: '10', gameName: 'Early Game', points: 10, eventAt: '2026-09-30T10:00:00Z'},
|
||||||
|
{kind: 'FINISHED_TOPUP', ref: '10', gameName: 'Early Game', points: 20, eventAt: '2026-09-30T10:00:00Z'},
|
||||||
|
]}))});
|
||||||
|
|
||||||
|
const links = screen.getAllByRole('link', {name: 'Early Game'});
|
||||||
|
expect(links).toHaveLength(2);
|
||||||
|
expect(links[0].getAttribute('href')).toBe('/library/10');
|
||||||
|
});
|
||||||
|
|
||||||
it('buys, then loads the wallet again', async () => {
|
it('buys, then loads the wallet again', async () => {
|
||||||
let balance = 160;
|
let balance = 160;
|
||||||
const mock = await renderPageWith(() => json(200, wallet({balance})), {
|
const mock = await renderPageWith(() => json(200, wallet({balance})), {
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import PageContainer from '../components/PageContainer.jsx';
|
|||||||
import QuestionCard from '../components/QuestionCard.jsx';
|
import QuestionCard from '../components/QuestionCard.jsx';
|
||||||
import usePointsCheck from '../hooks/usePointsCheck.js';
|
import usePointsCheck from '../hooks/usePointsCheck.js';
|
||||||
import {answerQuestion, fetchQuestions} from '../utils/questions.js';
|
import {answerQuestion, fetchQuestions} from '../utils/questions.js';
|
||||||
|
import {loadExpected} from '../utils/rewards.js';
|
||||||
|
|
||||||
|
// Core answers at most this many appids at once.
|
||||||
|
const MAX_EXPECTED = 200;
|
||||||
const STATUS_OF_QUESTION = {START: 'PLAYING', FINISHED: 'FINISHED', DROPPED: 'DROPPED'};
|
const STATUS_OF_QUESTION = {START: 'PLAYING', FINISHED: 'FINISHED', DROPPED: 'DROPPED'};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,6 +23,7 @@ export default function QuestionsPage() {
|
|||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [status, setStatus] = useState('loading');
|
const [status, setStatus] = useState('loading');
|
||||||
const [busyAppid, setBusyAppid] = useState(null);
|
const [busyAppid, setBusyAppid] = useState(null);
|
||||||
|
const [expected, setExpected] = useState({});
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
const {ok, status: code, data} = await fetchQuestions();
|
const {ok, status: code, data} = await fetchQuestions();
|
||||||
@@ -30,6 +34,12 @@ export default function QuestionsPage() {
|
|||||||
}
|
}
|
||||||
setItems(data.results);
|
setItems(data.results);
|
||||||
setStatus('ready');
|
setStatus('ready');
|
||||||
|
// Points are a hint: a failed call shows no line and no error.
|
||||||
|
const finished = data.results.filter((item) => item.question === 'FINISHED').map((item) => item.appid);
|
||||||
|
for (let from = 0; from < finished.length; from += MAX_EXPECTED) {
|
||||||
|
const points = await loadExpected(finished.slice(from, from + MAX_EXPECTED));
|
||||||
|
if (points.ok) setExpected((current) => ({...current, ...points.data}));
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -71,7 +81,7 @@ export default function QuestionsPage() {
|
|||||||
<p className="text-sm text-muted-foreground">{t('QUESTIONS.EMPTY')}</p>
|
<p className="text-sm text-muted-foreground">{t('QUESTIONS.EMPTY')}</p>
|
||||||
)}
|
)}
|
||||||
{status === 'ready' && items.map((item) => (
|
{status === 'ready' && items.map((item) => (
|
||||||
<QuestionCard key={item.appid} item={item} busy={busyAppid !== null}
|
<QuestionCard key={item.appid} item={item} busy={busyAppid !== null} expectedPoints={expected[item.appid]}
|
||||||
onAnswer={(choice) => answer(item, choice)}/>
|
onAnswer={(choice) => answer(item, choice)}/>
|
||||||
))}
|
))}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ const card = (appid, question, name = `Game ${appid}`) => ({appid, name, imageUr
|
|||||||
playtimeMinutes: 1500, normallyMinutes: null, lastActivity: null});
|
playtimeMinutes: 1500, normallyMinutes: null, lastActivity: null});
|
||||||
|
|
||||||
const booked = (appid) => ({balance: 30, missing: {}, paid: [{kind: 'FINISHED', appid, points: 30, paidAt: '2026-10-09T10:00:00Z', bookedNow: true}]});
|
const booked = (appid) => ({balance: 30, missing: {}, paid: [{kind: 'FINISHED', appid, points: 30, paidAt: '2026-10-09T10:00:00Z', bookedNow: true}]});
|
||||||
const WALLET = {'GET /api/rewards/wallet': json(200, {balance: 30, catalog: [], owned: [], equipped: {}, history: []})};
|
const WALLET = {
|
||||||
|
'GET /api/rewards/wallet': json(200, {balance: 30, catalog: [], owned: [], equipped: {}, history: []}),
|
||||||
|
'GET /api/rewards/expected': json(200, {}),
|
||||||
|
};
|
||||||
|
const expectedCalls = () => globalThis.fetch.mock.calls.filter(([url]) => url.includes('/api/rewards/expected'));
|
||||||
const pointsCalls = () => globalThis.fetch.mock.calls.filter(([url]) => url.includes('/api/rewards/games/'));
|
const pointsCalls = () => globalThis.fetch.mock.calls.filter(([url]) => url.includes('/api/rewards/games/'));
|
||||||
|
|
||||||
const renderPage = (routes) => {
|
const renderPage = (routes) => {
|
||||||
@@ -161,6 +165,45 @@ describe('QuestionsPage', () => {
|
|||||||
expect(screen.getByText('Game 1')).toBeTruthy();
|
expect(screen.getByText('Game 1')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('showsTheExpectedPointsOnAFinishedCardOnly', async () => {
|
||||||
|
renderPage({
|
||||||
|
'GET /api/library/questions': json(200, {results: [card(1, 'FINISHED'), card(2, 'FINISHED'), card(3, 'START')]}),
|
||||||
|
'GET /api/rewards/expected': json(200, {1: 50, 2: 0}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await screen.findByText('QUESTIONS.FINISHED.POINTS:50')).toBeTruthy();
|
||||||
|
expect(screen.getAllByText(/^QUESTIONS.FINISHED.POINTS/)).toHaveLength(1);
|
||||||
|
expect(expectedCalls()[0][0]).toContain('appids=1,2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('asksForTheExpectedPointsOfEveryFinishedCardInChunks', async () => {
|
||||||
|
const cards = Array.from({length: 201}, (_, i) => card(i + 1, 'FINISHED'));
|
||||||
|
renderPage({'GET /api/library/questions': json(200, {results: cards})});
|
||||||
|
|
||||||
|
await waitFor(() => expect(expectedCalls().length).toBeGreaterThan(0));
|
||||||
|
const asked = expectedCalls().flatMap(([url]) => new URL(url, 'http://x').searchParams.get('appids').split(','));
|
||||||
|
expect(asked).toContain('201');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('asksForNoExpectedPointsWithoutAFinishedCard', async () => {
|
||||||
|
renderPage({'GET /api/library/questions': json(200, {results: [card(1, 'START')]})});
|
||||||
|
await screen.findByText('Game 1');
|
||||||
|
|
||||||
|
expect(expectedCalls()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('showsNoPointsLineAndNoErrorWhenTheExpectedCallFails', async () => {
|
||||||
|
renderPage({
|
||||||
|
'GET /api/library/questions': json(200, {results: [card(1, 'FINISHED')]}),
|
||||||
|
'GET /api/rewards/expected': json(500, {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await screen.findByText('Game 1')).toBeTruthy();
|
||||||
|
await waitFor(() => expect(expectedCalls()).toHaveLength(1));
|
||||||
|
expect(screen.queryByText(/^QUESTIONS.FINISHED.POINTS/)).toBeNull();
|
||||||
|
expect(screen.queryByText('INFO.ERROR.NETWORK_FAILURE')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('showsTheEmptyState', async () => {
|
it('showsTheEmptyState', async () => {
|
||||||
renderPage({'GET /api/library/questions': json(200, {results: []})});
|
renderPage({'GET /api/library/questions': json(200, {results: []})});
|
||||||
|
|
||||||
|
|||||||
+11
-6
@@ -123,28 +123,33 @@ export const loadWallet = () => request(`${REWARDS_URL}/wallet`);
|
|||||||
/** Settles in Core, then answers the paid rows of the game and why a kind is missing: `{balance, paid, missing}`. */
|
/** Settles in Core, then answers the paid rows of the game and why a kind is missing: `{balance, paid, missing}`. */
|
||||||
export const loadGamePoints = (appid) => request(`${REWARDS_URL}/games/${appid}`);
|
export const loadGamePoints = (appid) => request(`${REWARDS_URL}/games/${appid}`);
|
||||||
|
|
||||||
const FINISH_KINDS = ['FINISHED', 'FINISHED_MANUAL'];
|
/** The full finish value per game, read-only: `{ok, status, data: {appid: points}}`. */
|
||||||
|
export const loadExpected = (appids) => request(`${REWARDS_URL}/expected?appids=${appids.join(',')}`);
|
||||||
|
|
||||||
|
const FINISH_KINDS = ['FINISHED', 'FINISHED_MANUAL', 'FINISHED_EARLY'];
|
||||||
|
|
||||||
// The kind of points a status can earn; the other statuses (BACKLOG, DROPPED) earn none.
|
// The kind of points a status can earn; the other statuses (BACKLOG, DROPPED) earn none.
|
||||||
const KIND_OF_STATUS = {PLAYING: 'STARTED', FINISHED: 'FINISHED', COMPLETED: 'COMPLETED'};
|
const KIND_OF_STATUS = {PLAYING: 'STARTED', FINISHED: 'FINISHED', COMPLETED: 'COMPLETED'};
|
||||||
const BLOCKED = ['HIDDEN', 'PLAYSTATION', 'PLAYSTATION_LINKED'];
|
const BLOCKED = ['HIDDEN', 'PLAYSTATION'];
|
||||||
/** The codes that only say the status never got there: no text, no line. */
|
/** The codes that only say the status never got there: no text, no line. */
|
||||||
export const STATUS_NOT_REACHED = ['NOT_STARTED', 'NOT_FINISHED', 'NOT_COMPLETED'];
|
export const STATUS_NOT_REACHED = ['NOT_STARTED', 'NOT_FINISHED', 'NOT_COMPLETED'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What to tell the user after a status save, from the answer of `loadGamePoints`:
|
* What to tell the user after a status save, from the answer of `loadGamePoints`:
|
||||||
* `{type: 'booked', points}` (rows written by that call), `{type: 'paid', date}`, `{type: 'none', reason}` (a code
|
* `{type: 'booked', points}` (rows written by that call), `{type: 'paid', date}`, `{type: 'none', reason}` (a code
|
||||||
* of Core) or `{type: 'status'}` (this status earns no points).
|
* of Core) or `{type: 'status'}` (this status earns no points). `rest: true` on booked and paid: a finish is paid
|
||||||
|
* early and the rest waits for proof.
|
||||||
*/
|
*/
|
||||||
export const pointsNotice = (points, status) => {
|
export const pointsNotice = (points, status) => {
|
||||||
|
const {missing} = points;
|
||||||
|
const rest = (status === 'FINISHED' || status === 'COMPLETED') && Boolean(missing.FINISHED_TOPUP);
|
||||||
const booked = points.paid.filter((row) => row.bookedNow).reduce((sum, row) => sum + row.points, 0);
|
const booked = points.paid.filter((row) => row.bookedNow).reduce((sum, row) => sum + row.points, 0);
|
||||||
if (booked > 0) return {type: 'booked', points: booked};
|
if (booked > 0) return {type: 'booked', points: booked, ...(rest && {rest})};
|
||||||
const kind = KIND_OF_STATUS[status];
|
const kind = KIND_OF_STATUS[status];
|
||||||
if (!kind) return {type: 'status'};
|
if (!kind) return {type: 'status'};
|
||||||
const kinds = kind === 'FINISHED' ? FINISH_KINDS : [kind];
|
const kinds = kind === 'FINISHED' ? FINISH_KINDS : [kind];
|
||||||
const row = points.paid.find((paid) => kinds.includes(paid.kind));
|
const row = points.paid.find((paid) => kinds.includes(paid.kind));
|
||||||
if (row) return {type: 'paid', date: row.paidAt};
|
if (row) return {type: 'paid', date: row.paidAt, ...(rest && {rest})};
|
||||||
const {missing} = points;
|
|
||||||
// A completion rests on a finish: its own reason comes after the reason of the finish.
|
// A completion rests on a finish: its own reason comes after the reason of the finish.
|
||||||
const reasons = kind === 'COMPLETED' ? [missing.FINISHED, missing.COMPLETED] : [missing[kind]];
|
const reasons = kind === 'COMPLETED' ? [missing.FINISHED, missing.COMPLETED] : [missing[kind]];
|
||||||
const reason = [...reasons, Object.values(missing).find((code) => BLOCKED.includes(code))]
|
const reason = [...reasons, Object.values(missing).find((code) => BLOCKED.includes(code))]
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ describe('pointsNotice', () => {
|
|||||||
expect(pointsNotice(points, 'FINISHED')).toEqual({type: 'paid', date: '2026-10-01T10:00:00Z'});
|
expect(pointsNotice(points, 'FINISHED')).toEqual({type: 'paid', date: '2026-10-01T10:00:00Z'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('counts an early finish as a paid finish and says the rest waits for proof', () => {
|
||||||
|
const points = {paid: [row('FINISHED_EARLY', {points: 10})], missing: {FINISHED_TOPUP: 'NO_PROOF'}};
|
||||||
|
|
||||||
|
expect(pointsNotice(points, 'FINISHED')).toEqual({type: 'paid', date: '2026-10-01T10:00:00Z', rest: true});
|
||||||
|
expect(pointsNotice({...points, missing: {...points.missing, COMPLETED: 'NEEDS_PROVEN_FINISH'}}, 'COMPLETED'))
|
||||||
|
.toEqual({type: 'none', reason: 'NEEDS_PROVEN_FINISH'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks booked early points with rest', () => {
|
||||||
|
const points = {paid: [row('FINISHED_EARLY', {points: 10, bookedNow: true})], missing: {FINISHED_TOPUP: 'NO_PROOF'}};
|
||||||
|
|
||||||
|
expect(pointsNotice(points, 'FINISHED')).toEqual({type: 'booked', points: 10, rest: true});
|
||||||
|
expect(pointsNotice({paid: [row('FINISHED_TOPUP', {points: 20, bookedNow: true})], missing: {}}, 'FINISHED'))
|
||||||
|
.toEqual({type: 'booked', points: 20});
|
||||||
|
});
|
||||||
|
|
||||||
it('gives the reason of the missing kind', () => {
|
it('gives the reason of the missing kind', () => {
|
||||||
expect(pointsNotice({paid: [], missing: {FINISHED: 'NO_PROOF'}}, 'FINISHED')).toEqual({type: 'none', reason: 'NO_PROOF'});
|
expect(pointsNotice({paid: [], missing: {FINISHED: 'NO_PROOF'}}, 'FINISHED')).toEqual({type: 'none', reason: 'NO_PROOF'});
|
||||||
expect(pointsNotice({paid: [], missing: {FINISHED: 'HIDDEN'}}, 'PLAYING')).toEqual({type: 'none', reason: 'HIDDEN'});
|
expect(pointsNotice({paid: [], missing: {FINISHED: 'HIDDEN'}}, 'PLAYING')).toEqual({type: 'none', reason: 'HIDDEN'});
|
||||||
|
|||||||
Reference in new issue
Block a user