Add SRS app: results, shops, orders and users, wired to srs-api
Flutter web/PWA for the lottery agency, built on Riverpod and go_router,
talking to srs-api over HTTP (HttpApi, token kept in SharedPreferences).
- Results, Shops, Orders and Users tabs (Users for admins only), with
phone, tablet and desktop layouts and shared widgets and theme tokens
- Users: list with search and Active/Inactive filters; user detail with
shops, sign-in details, and an Access card
- WhatsApp results switch on each user (sendResults on
PATCH /api/users/:id). Shows the WhatsApp logo (Simple Icons, CC0,
drawn with flutter_svg), green only when results will really go out.
Can't be turned on without a phone number ("Add a phone number first"
opens the phone sheet), and is paused while the user is inactive. The
users list marks people who get results with the logo.
- Each Access switch has a one-line description; the explanation
paragraphs are gone
- Remove demo mode: the app always uses the real AP
test/support and is used only by tests.
211 lines
6.9 KiB
Dart
211 lines
6.9 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:srs_app/core/api.dart';
|
|
import 'support/fake_api.dart';
|
|
import 'package:srs_app/core/format.dart';
|
|
|
|
void main() {
|
|
group('format', () {
|
|
test('Indian digit grouping', () {
|
|
expect(inr(0), '0');
|
|
expect(inr(999), '999');
|
|
expect(inr(1000), '1,000');
|
|
expect(inr(7700), '7,700');
|
|
expect(inr(519900), '5,19,900');
|
|
expect(inr(12345678), '1,23,45,678');
|
|
expect(rupees(3700), '₹3,700');
|
|
});
|
|
|
|
test('dates and times', () {
|
|
expect(dateLong(DateTime(2026, 10, 16)), 'Fri, 16 Oct 2026');
|
|
expect(dateShort(DateTime(2026, 10, 16)), 'Fri, 16 Oct');
|
|
expect(time12(DateTime(2026, 10, 16, 12, 41)), '12:41 PM');
|
|
expect(time12(DateTime(2026, 10, 16, 0, 5)), '12:05 AM');
|
|
expect(fmtPhone('919000012345'), '+91 90000 12345');
|
|
});
|
|
|
|
test('server instants are shown on the Indian clock', () {
|
|
expect(
|
|
istWallClock(DateTime.utc(2026, 10, 16, 5, 30)),
|
|
DateTime(2026, 10, 16, 11, 0),
|
|
);
|
|
expect(
|
|
istWallClock(DateTime.utc(2026, 10, 16, 20, 0)),
|
|
DateTime(2026, 10, 17, 1, 30),
|
|
);
|
|
expect(parseDate('2026-10-16'), DateTime(2026, 10, 16));
|
|
});
|
|
|
|
test('prize tiers read naturally', () {
|
|
expect(tiersLabel([5]), '5th prize');
|
|
expect(tiersLabel([8, 5]), '5th + 8th prize');
|
|
expect(tiersLabel([]), '');
|
|
});
|
|
});
|
|
|
|
group('FakeApi fixture (SK-74, Fri 16 Oct 2026)', () {
|
|
final d = DateTime(2026, 10, 16);
|
|
|
|
test('admin sees the design numbers', () async {
|
|
final api = FakeApi();
|
|
await api.login('owner', 'admin123');
|
|
final r = await api.results(d);
|
|
expect(r.draw.published, isTrue);
|
|
expect(r.draw.label, 'Suvarna Keralam · SK-74');
|
|
expect(r.totalPrize, 7700);
|
|
expect(r.winningTickets, 16);
|
|
expect(r.totalTickets, 500);
|
|
expect(r.orders, 6);
|
|
expect(r.winningShops, 4);
|
|
expect(r.topShop!.shop.code, 'KZK');
|
|
expect(r.topShop!.prize, 3700);
|
|
expect(r.topShop!.winTickets, 10);
|
|
});
|
|
|
|
test('KZK detail: 180 tickets, 52 endings, 2 orders', () async {
|
|
final api = FakeApi();
|
|
await api.login('owner', 'admin123');
|
|
final shops = await api.shops(d);
|
|
final kzk = shops.firstWhere((s) => s.shop.code == 'KZK');
|
|
expect(kzk.tickets, 180);
|
|
expect(kzk.endings, 52);
|
|
expect(kzk.orders, 2);
|
|
final det = await api.shopDetail(kzk.shop.id, d);
|
|
expect(det.prize, 3700);
|
|
expect(det.wins.map((w) => w.ending).toSet(), {
|
|
'0376',
|
|
'0987',
|
|
'1360',
|
|
'2564',
|
|
});
|
|
});
|
|
|
|
test('results for tomorrow are not out; counts only', () async {
|
|
final api = FakeApi();
|
|
await api.login('owner', 'admin123');
|
|
final r = await api.results(DateTime(2026, 10, 17));
|
|
expect(r.draw.published, isFalse);
|
|
expect(r.totalPrize, 0);
|
|
expect(r.totalTickets, 30);
|
|
});
|
|
});
|
|
|
|
group('roles', () {
|
|
final d = DateTime(2026, 10, 16);
|
|
|
|
test('user only sees assigned shops', () async {
|
|
final api = FakeApi();
|
|
await api.login('kzk.staff', 'staff123');
|
|
final r = await api.results(d);
|
|
expect(r.shops.map((s) => s.shop.code).toSet(), {'KZK', 'SKM'});
|
|
expect(r.totalPrize, 3700 + 2600);
|
|
final shops = await api.shops(d);
|
|
expect(shops.length, 2);
|
|
expect(() => api.shopDetail(3, d), throwsA(isA<ApiException>()));
|
|
});
|
|
|
|
test('user cannot manage shops or users', () async {
|
|
final api = FakeApi();
|
|
await api.login('kzk.staff', 'staff123');
|
|
expect(() => api.createShop('X', 'XXX'), throwsA(isA<ApiException>()));
|
|
expect(() => api.users(), throwsA(isA<ApiException>()));
|
|
});
|
|
|
|
test('wrong password and inactive users cannot sign in', () async {
|
|
final api = FakeApi();
|
|
expect(() => api.login('owner', 'nope'), throwsA(isA<ApiException>()));
|
|
expect(
|
|
() => api.login('ptm.staff', 'staff123'),
|
|
throwsA(isA<ApiException>()),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('editing', () {
|
|
final d = DateTime(2026, 10, 16);
|
|
|
|
test(
|
|
'changing a quantity updates totals and marks the order edited',
|
|
() async {
|
|
final api = FakeApi();
|
|
await api.login('owner', 'admin123');
|
|
final orders = await api.orders(d);
|
|
final ptm = orders.firstWhere((o) => o.shop.code == 'PTM');
|
|
await api.setLineQty(ptm.id, '3821', 3);
|
|
final r = await api.results(d);
|
|
expect(r.totalPrize, 7700 + 400);
|
|
expect((await api.order(ptm.id)).edited, isTrue);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'removing the last ending deletes the order; cancel removes tickets',
|
|
() async {
|
|
final api = FakeApi();
|
|
await api.login('owner', 'admin123');
|
|
final orders = await api.orders(DateTime(2026, 10, 17));
|
|
final one = orders.firstWhere((o) => o.shop.code == 'KZK');
|
|
for (final l in one.lines) {
|
|
await api.removeLine(one.id, l.ending);
|
|
}
|
|
expect(() => api.order(one.id), throwsA(isA<ApiException>()));
|
|
final ulr = orders.firstWhere((o) => o.shop.code == 'ULR');
|
|
await api.cancelOrder(ulr.id);
|
|
expect((await api.orders(DateTime(2026, 10, 17))), isEmpty);
|
|
},
|
|
);
|
|
|
|
test('shop and user rules', () async {
|
|
final d = DateTime(2026, 10, 16);
|
|
final api = FakeApi();
|
|
await api.login('owner', 'admin123');
|
|
expect(
|
|
() => api.createShop('Another', 'KZK'),
|
|
throwsA(isA<ApiException>()),
|
|
);
|
|
final s = await api.createShop('Medical College', 'MCH');
|
|
await api.renameShop(s.id, 'Medical College Jn');
|
|
await api.setShopActive(s.id, false);
|
|
expect((await api.activeShops()).any((x) => x.id == s.id), isFalse);
|
|
expect(
|
|
(await api.shops(d)).any((x) => x.shop.id == s.id && !x.shop.active),
|
|
isTrue,
|
|
);
|
|
await api.setShopActive(s.id, true);
|
|
expect((await api.activeShops()).any((x) => x.id == s.id), isTrue);
|
|
|
|
expect(
|
|
() =>
|
|
api.createUser(username: 'ab', password: 'longenough', shopIds: []),
|
|
throwsA(isA<ApiException>()),
|
|
);
|
|
expect(
|
|
() =>
|
|
api.createUser(username: 'newuser', password: 'short', shopIds: []),
|
|
throwsA(isA<ApiException>()),
|
|
);
|
|
final u = await api.createUser(
|
|
username: 'newuser',
|
|
phone: '98765 43210',
|
|
password: 'longenough',
|
|
shopIds: [1],
|
|
);
|
|
expect(u.phone, '919876543210');
|
|
await api.updateUser(u.id, active: false);
|
|
await api.deleteUser(u.id);
|
|
expect((await api.users()).any((x) => x.id == u.id), isFalse);
|
|
expect(() => api.deleteUser(1), throwsA(isA<ApiException>()));
|
|
final u2 = await api.createUser(
|
|
username: 'second',
|
|
password: 'longenough',
|
|
shopIds: [],
|
|
);
|
|
await api.updateUser(u2.id, active: false);
|
|
api.logout();
|
|
expect(
|
|
() => api.login('second', 'longenough'),
|
|
throwsA(isA<ApiException>()),
|
|
);
|
|
});
|
|
});
|
|
}
|