瀏覽代碼

ссылки на пополнение, отслеживание времени до пересчёта осталось дело

κρμγ 5 年之前
父節點
當前提交
1045320374
共有 29 個文件被更改,包括 774 次插入80 次删除
  1. 2 2
      src/app/package.json
  2. 12 9
      src/app/src/app/app.component.html
  3. 8 1
      src/app/src/app/app.component.ts
  4. 36 6
      src/app/src/app/app.module.ts
  5. 51 25
      src/app/src/app/bank/account/account.service.ts
  6. 37 0
      src/app/src/app/bank/bank.service.ts
  7. 38 0
      src/app/src/app/misc/DefaultErrorHandler.ts
  8. 58 0
      src/app/src/app/misc/PendingIndicator.ts
  9. 38 0
      src/app/src/app/person/person.debit.dialog.component.html
  10. 21 0
      src/app/src/app/person/person.debit.dialog.component.scss
  11. 65 12
      src/app/src/app/person/person.page.component.html
  12. 4 0
      src/app/src/app/person/person.page.component.scss
  13. 92 5
      src/app/src/app/person/person.page.component.ts
  14. 二進制
      src/app/src/assets/account.png
  15. 26 0
      src/main/java/inn/ocsf/bee/freigeld/core/model/PublicLink.java
  16. 10 0
      src/main/java/inn/ocsf/bee/freigeld/core/model/data/TicketData.java
  17. 7 0
      src/main/java/inn/ocsf/bee/freigeld/core/repo/PublicLinkRepository.java
  18. 11 0
      src/main/java/inn/ocsf/bee/freigeld/core/repo/PublicLinkRepositoryCustom.java
  19. 3 3
      src/main/java/inn/ocsf/bee/freigeld/core/repo/TicketRepository.java
  20. 4 0
      src/main/java/inn/ocsf/bee/freigeld/utils/ZBase32Utils.java
  21. 23 3
      src/main/kotlin/inn/ocsf/bee/freigeld/core/data/CentralBankQueueLevel.kt
  22. 53 3
      src/main/kotlin/inn/ocsf/bee/freigeld/core/data/GlobalEmitterImpl.kt
  23. 5 3
      src/main/kotlin/inn/ocsf/bee/freigeld/core/demo/DemoInMem.kt
  24. 1 1
      src/main/kotlin/inn/ocsf/bee/freigeld/core/repo/CoinRepositoryImpl.kt
  25. 40 0
      src/main/kotlin/inn/ocsf/bee/freigeld/core/repo/PublicLinkRepositoryImpl.kt
  26. 20 3
      src/main/kotlin/inn/ocsf/bee/freigeld/core/service/TicketService.kt
  27. 27 0
      src/main/kotlin/inn/ocsf/bee/freigeld/serve/rest/BankController.kt
  28. 81 4
      src/main/kotlin/inn/ocsf/bee/freigeld/serve/rest/PersonController.kt
  29. 1 0
      src/main/kotlin/inn/ocsf/bee/freigeld/utils/KeyValueStorage.kt

+ 2 - 2
src/app/package.json

@@ -20,12 +20,12 @@
     "@angular/router": "~9.1.4",
     "@auth0/angular-jwt": "^4.0.0",
     "moment": "^2.25.3",
+    "ngx-init": "^0.1.1",
     "rxjs": "~6.5.4",
     "tslib": "^1.10.0",
     "underscore": "^1.10.2",
     "uuid": "^8.0.0",
-    "zone.js": "~0.10.2",
-    "ngx-init": "^0.1.1"
+    "zone.js": "~0.10.2"
   },
   "devDependencies": {
     "@angular-devkit/build-angular": "~0.901.4",

+ 12 - 9
src/app/src/app/app.component.html

@@ -1,14 +1,17 @@
 <mat-toolbar class="mat-elevation-z6" color="primary">
-  <mat-toolbar-row fxLayout="row" fxLayoutAlign="space-between center">
-    <span><span [routerLink]="['/']" class="href"><img alt="logo" src="../assets/favicon-16x16.png">&nbsp;СВОБОДНЫЕ ДЕНЬГИ</span></span>
+  <mat-toolbar-row fxLayout="row" fxLayoutAlign="start center">
+    <mat-icon>local_activity</mat-icon>&nbsp;
+    <div fxFlex="100" fxLayout="row" fxLayoutAlign="space-between center">
+      <span [routerLink]="['/']" class="href">СВОБОДНЫЕ ДЕНЬГИ</span>
 
-    <div fxLayout="row">
-      <button *ngIf="hasAuth() | async" [routerLink]="['/personal']" mat-button>
-        <mat-icon>account_circle</mat-icon>&nbsp;КАБИНЕТ
-      </button>
-      <div [ngSwitch]="hasAuth() | async">
-        <button (click)="logout()" *ngSwitchCase="true" mat-button>ВЫЙТИ</button>
-        <button *ngSwitchDefault [routerLink]="['/login']" mat-button>ВОЙТИ</button>
+      <div fxLayout="row">
+        <button *ngIf="hasAuth() | async" [routerLink]="['/personal']" mat-button>
+          <mat-icon>account_circle</mat-icon>&nbsp;КАБИНЕТ
+        </button>
+        <div [ngSwitch]="hasAuth() | async">
+          <button (click)="logout()" *ngSwitchCase="true" mat-button>ВЫЙТИ</button>
+          <button *ngSwitchDefault [routerLink]="['/login']" mat-button>ВОЙТИ</button>
+        </div>
       </div>
     </div>
   </mat-toolbar-row>

+ 8 - 1
src/app/src/app/app.component.ts

@@ -4,6 +4,7 @@ import {PersonService} from "./person/person.service";
 import {map} from "rxjs/operators";
 import * as _ from 'underscore'
 import {Router} from "@angular/router";
+import {Title} from "@angular/platform-browser";
 
 @Component({
   selector: 'app-root',
@@ -12,7 +13,9 @@ import {Router} from "@angular/router";
 })
 export class AppComponent {
 
-  constructor(private personService: PersonService, private router: Router) {
+  public title = "FREIGELD"
+
+  constructor(private titleService: Title, private personService: PersonService, private router: Router) {
 
   }
 
@@ -24,4 +27,8 @@ export class AppComponent {
     this.personService.resetCurrentPerson()
     this.router.navigateByUrl("/")
   }
+
+  setTitle(title: string) {
+    this.titleService.setTitle(`${title} :: ${this.title}`);
+  }
 }

+ 36 - 6
src/app/src/app/app.module.ts

@@ -1,5 +1,5 @@
 import {BrowserModule} from '@angular/platform-browser';
-import {LOCALE_ID, NgModule} from '@angular/core';
+import {ErrorHandler, LOCALE_ID, NgModule} from '@angular/core';
 
 import {AppRoutingModule} from './app-routing.module';
 import {AppComponent} from './app.component';
@@ -13,7 +13,7 @@ import {MatCardModule} from "@angular/material/card";
 import {MAT_SNACK_BAR_DEFAULT_OPTIONS, MatSnackBarModule} from "@angular/material/snack-bar";
 import {registerLocaleData} from "@angular/common";
 import localeRu from '@angular/common/locales/ru'
-import {HttpClientModule} from "@angular/common/http";
+import {HTTP_INTERCEPTORS, HttpClientModule} from "@angular/common/http";
 import {MatFormFieldModule} from "@angular/material/form-field";
 import {MatInputModule} from "@angular/material/input";
 import {FormsModule} from "@angular/forms";
@@ -21,9 +21,20 @@ import {MatIconModule} from "@angular/material/icon";
 import {PersonService} from "./person/person.service";
 import {JwtModule} from "@auth0/angular-jwt";
 import {NgxInitModule} from "ngx-init";
-import {PersonPageComponent} from "./person/person.page.component";
+import {PersonDebitDialogComponent, PersonPageComponent} from "./person/person.page.component";
 import {MatListModule} from "@angular/material/list";
-import {AccountService} from "./account/account.service";
+import {AccountService} from "./bank/account/account.service";
+import {MatProgressBarModule} from "@angular/material/progress-bar";
+import {BankService} from "./bank/bank.service";
+import {MAT_TOOLTIP_SCROLL_STRATEGY, MatTooltipModule} from "@angular/material/tooltip";
+import {NoopScrollStrategy} from "@angular/cdk/overlay";
+import {MatDialogModule} from "@angular/material/dialog";
+import {MatSelectModule} from "@angular/material/select";
+import {MatSliderModule} from "@angular/material/slider";
+import {MatRadioModule} from "@angular/material/radio";
+import {MatCheckboxModule} from "@angular/material/checkbox";
+import {DefaultErrorHandler} from "./misc/DefaultErrorHandler";
+import {PendingIndicator, PendingSnackBar} from "./misc/PendingIndicator";
 
 registerLocaleData(localeRu);
 
@@ -33,7 +44,10 @@ export function jwtTokenGetter() {
 
 @NgModule({
   declarations: [
-    AppComponent, AppLoginComponent, AppIndexComponent, PersonPageComponent
+    AppComponent, AppLoginComponent, AppIndexComponent, PersonPageComponent, PersonDebitDialogComponent, PendingSnackBar
+  ],
+  entryComponents: [
+    PersonDebitDialogComponent, PendingSnackBar
   ],
   imports: [
     BrowserModule,
@@ -58,12 +72,28 @@ export function jwtTokenGetter() {
       },
     }),
     MatListModule,
+    MatProgressBarModule,
+    MatTooltipModule,
+    MatDialogModule,
+    MatSelectModule,
+    MatSliderModule,
+    MatRadioModule,
+    MatCheckboxModule,
   ],
   providers: [
     PersonService,
     AccountService,
+    BankService,
+    {provide: ErrorHandler, useClass: DefaultErrorHandler},
     {provide: LOCALE_ID, useValue: "ru-RU"},
-    {provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: {duration: 2500}}
+    {provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: {duration: 2500}},
+    {provide: MAT_TOOLTIP_SCROLL_STRATEGY, useValue: () => new NoopScrollStrategy()},
+    {
+      provide: HTTP_INTERCEPTORS,
+      useClass: PendingIndicator,
+      multi: true
+    },
+
   ],
   bootstrap: [AppComponent]
 })

+ 51 - 25
src/app/src/app/account/account.service.ts → src/app/src/app/bank/account/account.service.ts

@@ -1,25 +1,51 @@
-import {Injectable} from "@angular/core";
-import {HttpClient} from "@angular/common/http";
-import {Observable} from "rxjs";
-import {map} from "rxjs/operators";
-
-@Injectable({providedIn: "root"})
-export class AccountService {
-
-  constructor(private httpClient: HttpClient) {
-
-  }
-
-  getAccountsInfo(personId: string): Observable<AccountInfo[]> {
-    return this.httpClient.get(`/api/old/person/${personId}/accounts`).pipe(map((res: any) => <AccountInfo[]>res.accounts))
-  }
-
-  addAccount(personId: string): Observable<string> {
-    return this.httpClient.post(`/api/old/person/${personId}/accounts/add`, {}).pipe(map((res: any) => <string>res.id))
-  }
-}
-
-export class AccountInfo {
-  id: string
-  overall: number
-}
+import {Injectable} from "@angular/core";
+import {HttpClient} from "@angular/common/http";
+import {Observable} from "rxjs";
+import {map} from "rxjs/operators";
+import * as moment from 'moment'
+import * as _ from 'underscore'
+
+@Injectable({providedIn: "root"})
+export class AccountService {
+
+  constructor(private httpClient: HttpClient) {
+
+  }
+
+  getAccountsInfo(personId: string): Observable<AccountInfo[]> {
+    return this.httpClient.get(`/api/old/person/${personId}/accounts`).pipe(map((res: any) => <AccountInfo[]>res.accounts))
+  }
+
+  addAccount(personId: string): Observable<string> {
+    return this.httpClient.post(`/api/old/person/${personId}/accounts/add`, {}).pipe(map((res: any) => <string>res.id))
+  }
+
+  getAccountEvents(personId: string): Observable<AccountEvent[]> {
+    return this.httpClient.get(`/api/old/person/${personId}/events`).pipe(map((res: any[]) => res.map(e => {
+      let ev = new AccountEvent()
+      ev.date = moment(e.ts).toDate()
+      ev.type = _.last(e.type.split("."))
+      ev.id = e.id
+      switch (ev.type) {
+        case "ExchangeSuccessEvent":
+          ev.info = e.info.parentEvent
+          break;
+        default:
+          throw new Error(`unknown event ${e.type}`)
+      }
+      return ev
+    })))
+  }
+}
+
+export class AccountInfo {
+  id: string
+  overall: number
+}
+
+export class AccountEvent {
+  id: string
+  type: string
+  date: Date
+  info: any
+}

+ 37 - 0
src/app/src/app/bank/bank.service.ts

@@ -0,0 +1,37 @@
+import {Injectable} from "@angular/core";
+import {HttpClient} from "@angular/common/http";
+import {Observable} from "rxjs";
+import {map} from "rxjs/operators";
+import * as moment from 'moment';
+import * as _ from 'underscore'
+
+@Injectable({providedIn: "root"})
+export class BankService {
+
+  constructor(private httpClient: HttpClient) {
+
+  }
+
+  getNextDemurrage(): Observable<DemurrageInfo> {
+    return this.httpClient.get("/api/bank/demurrage/next").pipe(map((res: any) => <DemurrageInfo[]>res)).pipe(map(r => _.first(r))).pipe(map((di: any) => new DemurrageInfo(moment(di.dt).toDate())))
+  }
+
+}
+
+export class DemurrageInfo {
+  constructor(public date: Date) {
+  }
+
+  getValue(): number {
+    let x = moment(this.date)
+    let n = moment.now()
+    let t = this.getTotal()
+    let dt = x.diff(n, 'seconds')
+    let ret = Math.round(100 * dt / t)
+    return 100 - ret
+  }
+
+  getTotal(): number {
+    return 7 * 24 * 60 * 60;
+  }
+}

+ 38 - 0
src/app/src/app/misc/DefaultErrorHandler.ts

@@ -0,0 +1,38 @@
+import {ErrorHandler, Injectable, Injector} from "@angular/core";
+import {HttpErrorResponse} from "@angular/common/http";
+import {MatSnackBar} from "@angular/material/snack-bar";
+import * as _ from 'underscore';
+
+@Injectable()
+export class DefaultErrorHandler implements ErrorHandler {
+
+  constructor(private injector: Injector) {
+  }
+
+  handleError(err: any): void {
+    if (_.has(err, 'promise')) {
+      err = err.rejection;
+    }
+    if (err instanceof HttpErrorResponse) {
+      let snackBar = this.injector.get(MatSnackBar);
+      switch (err.status) {
+        case 401:
+        case 403:
+          snackBar.open(err.status + ". Ошибка авторизации " + err.statusText);
+          break;
+        case 500:
+          snackBar.open("500. Произошла внутренняя ошибка сервера ");
+          break;
+        case 504:
+          snackBar.open("504. Сервер не отвечает");
+          break;
+        default:
+          snackBar.open(err.status + ". Неизвестная ошибка при входе в систему " + err.statusText);
+      }
+
+    } else {
+      console.error("DefaultErrorHandler: ", err);
+    }
+
+  }
+}

+ 58 - 0
src/app/src/app/misc/PendingIndicator.ts

@@ -0,0 +1,58 @@
+import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
+import {Component, Injectable, Injector} from "@angular/core";
+import {MatSnackBar} from "@angular/material/snack-bar";
+import {Observable} from "rxjs";
+import {tap} from 'rxjs/operators';
+
+@Component({
+  template: `
+    <section><p>Ожидание ответа от сервера</p>
+      <mat-progress-bar value="40" color="warn" mode="indeterminate"></mat-progress-bar>
+    </section>`,
+  styles: [`
+    p {
+      font-family: Arial, Helvetica, sans-serif;
+      text-align: center
+    }
+  `]
+})
+export class PendingSnackBar {
+}
+
+@Injectable()
+export class PendingIndicator implements HttpInterceptor {
+
+  private snackBarRef;
+
+  constructor(private injector: Injector) {
+  }
+
+  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+    console.log("Pending indicator");
+
+    var t = setTimeout(_ => {
+      this.show()
+    }, 500);
+
+    return next.handle(req).pipe(tap(() => {
+      clearTimeout(t);
+      console.log("Pending indicator finally");
+      this.close();
+    }))
+  }
+
+  private getSnackBar() {
+    return this.injector.get(MatSnackBar);
+  };
+
+  private show() {
+    this.snackBarRef = this.getSnackBar().openFromComponent(PendingSnackBar);
+  }
+
+  private close() {
+    if (this.snackBarRef !== undefined)
+      this.snackBarRef.dismiss();
+  }
+
+
+}

+ 38 - 0
src/app/src/app/person/person.debit.dialog.component.html

@@ -0,0 +1,38 @@
+<h2 mat-dialog-title>ПОПОЛНЕНИЕ</h2>
+<div class="person-debit-dialog-content" fxLayout="column" fxLayoutAlign="start stretch" fxLayoutGap="0.5em" mat-dialog-content>
+  <mat-form-field>
+    <mat-label>Счёт для пополнения</mat-label>
+    <mat-select [(value)]="data.account">
+      <mat-option *ngFor="let account of data.accounts" [value]="account">Баланс: {{account.overall}}</mat-option>
+    </mat-select>
+  </mat-form-field>
+  <mat-form-field>
+    <mat-label>Сумма</mat-label>
+    <input [(ngModel)]="data.amount" matInput min="1" step="1" type="number"/>
+    <mat-hint>Если оставить поле пустым, отправитель сможет сам указать нужную сумму</mat-hint>
+  </mat-form-field>
+  <div style="padding-top: 1em">
+    <mat-checkbox [(ngModel)]="data.once" labelPosition="after">сделать ссылку одноразовой</mat-checkbox>
+  </div>
+  <div fxLayout="column" fxLayoutGap="0.5em">
+    <mat-label id="time-radio-label">Срок действия</mat-label>
+    <mat-radio-group [(ngModel)]="data.time" [disabled]="!data.once" aria-labelledby="time-radio-label" fxLayout="column" fxLayoutGap="0.2em">
+      <mat-radio-button [value]="1">1 час</mat-radio-button>
+      <mat-radio-button [value]="3">3 часа</mat-radio-button>
+      <mat-radio-button [value]="8">8 часов</mat-radio-button>
+      <mat-radio-button [value]="24">1 день</mat-radio-button>
+      <mat-radio-button [value]="3 * 24">3 дня</mat-radio-button>
+      <mat-radio-button [value]="7 * 24">7 дней</mat-radio-button>
+    </mat-radio-group>
+  </div>
+  <div class="person-debit-dialog-code-panel" fxLayout="column" fxLayoutAlign="center center">
+    <p [ngClass]="{'person-debit-dialog-code-empty': !data.code, 'person-debit-dialog-code-full': !!data.code}">{{data.code ? data.code : 'КОД123'}}</p>
+  </div>
+  <mat-form-field *ngIf="!!data.code">
+    <input [readonly]="true" [value]="getCodeUrl(data.code)" matInput/>
+  </mat-form-field>
+</div>
+<div fxLayout="row" fxLayoutAlign="end center" mat-dialog-actions>
+  <button (click)="getCode()" color="warn" mat-button>СОЗДАТЬ КОД</button>
+  <button [mat-dialog-close]="{data: data}" mat-button>ОТМЕНА</button>
+</div>

+ 21 - 0
src/app/src/app/person/person.debit.dialog.component.scss

@@ -0,0 +1,21 @@
+.person-debit-dialog-content {
+  overflow: hidden;
+}
+
+.person-debit-dialog-code-panel {
+  min-height: 80px;
+}
+
+.person-debit-dialog-code {
+  font-size: xx-large;
+}
+
+.person-debit-dialog-code-full {
+  @extend .person-debit-dialog-code;
+  color: black;
+}
+
+.person-debit-dialog-code-empty {
+  @extend .person-debit-dialog-code;
+  color: lightgray;
+}

+ 65 - 12
src/app/src/app/person/person.page.component.html

@@ -1,22 +1,75 @@
-<div fxFlex="100" fxLayout="row" fxLayoutAlign="stretch start" fxLayoutGap="1em">
-  <div fxFlex="25" fxLayout="column">
+<div fxFlex="100" fxLayout="row" fxLayoutAlign="stretch start" fxLayoutGap="0.5em">
+  <div fxFlex="25" fxLayout="column" fxLayoutGap="0.5em">
+    <mat-toolbar>
+      <mat-toolbar-row>Аккаунт</mat-toolbar-row>
+    </mat-toolbar>
     <mat-card>
-      блабла о вас
+      <mat-card-header>
+        <div class="account-header-avatar" mat-card-avatar></div>
+        <mat-card-title>Свободный аккаунт</mat-card-title>
+        <mat-card-subtitle>Пульт управления свободными деньгами!</mat-card-subtitle>
+      </mat-card-header>
+      <mat-card-content>
+        <p>Целью ввода Свободных Денег является разрушение нечестных привилегии нынешних
+          денег. Эта нечестность целиком и полностью заключается в том, что наша традиционная
+          форма денег имеет одно неоспоримое и всегдашнее преимущество перед товарами, т. е.
+          то, что деньги "вечны". </p>
+      </mat-card-content>
       <mat-card-actions>
         <button (click)="addAccount()" mat-button>ОТКРЫТЬ СЧЁТ</button>
+        <button (click)="iNeedMoney()" mat-button>ДАЙ ДЕНЕГ!</button>
       </mat-card-actions>
     </mat-card>
+    <mat-toolbar>
+      <mat-toolbar-row>Счета</mat-toolbar-row>
+    </mat-toolbar>
+    <mat-list *ngxInit="{} as hovered">
+      <mat-list-item>
+        <mat-icon mat-list-icon>toll</mat-icon>
+        <span mat-line>Всего монет: {{getTotatOverall()}}</span>
+      </mat-list-item>
+      <mat-divider></mat-divider>
+      <mat-list-item (mouseenter)="hovered[idx] = true" (mouseleave)="hovered[idx] = false" *ngFor="let account of accounts; let idx=index">
+        <mat-icon mat-list-icon>toll</mat-icon>
+        <div fxLayout="row" fxLayoutAlign="space-between center" mat-line>
+          <span>Баланс: {{account.overall}}</span>
+          <button (click)="showDebitDialog(account)" [disabled]="!hovered[idx]" class="mat-elevation-z0" mat-mini-fab matTooltip="ПОПОЛНИТЬ СЧЁТ">
+            <mat-icon>add_circle_outline</mat-icon>
+          </button>
+        </div>
+      </mat-list-item>
+    </mat-list>
+  </div>
+  <div fxFlex="50" fxLayout="column" fxLayoutGap="0.5em">
+    <mat-toolbar>
+      <mat-toolbar-row>Операции</mat-toolbar-row>
+    </mat-toolbar>
+    <mat-list>
+      <mat-list-item *ngFor="let ev of events">
+        <mat-icon *ngIf="ev.info.direction == 'to'" mat-list-icon>archive</mat-icon>
+        <mat-icon *ngIf="ev.info.direction == 'from'" mat-list-icon>unarchive</mat-icon>
+        <div mat-line>Количество: {{ev.info.amount}}</div>
+        <div mat-line>
+          <span *ngIf="ev.info.direction == 'to'">пополнение</span>
+          <span *ngIf="ev.info.direction == 'from'">списание</span>
+          <span> от {{ev.date | date : 'medium'}}</span>
+        </div>
+      </mat-list-item>
+    </mat-list>
+  </div>
+  <div fxFlex="25" fxLayout="column" fxLayoutGap="0.5em">
+    <mat-toolbar>
+      <mat-toolbar-row>Разное</mat-toolbar-row>
+    </mat-toolbar>
     <mat-list>
-      <mat-list-item *ngFor="let account of accounts">
-        <span>Баланс: {{account.overall}}</span>
+      <mat-list-item *ngIf="demurrage">
+        <mat-icon mat-list-icon>bubble_chart</mat-icon>
+        <div mat-line>Пересчёт монет</div>
+        <div mat-line>Намечен на {{demurrage.date | date : 'medium'}}</div>
+        <div mat-line>
+          <mat-progress-bar [bufferValue]="demurrage.getValue()" [value]="demurrage.getValue()" color="warn" mode="buffer"></mat-progress-bar>
+        </div>
       </mat-list-item>
     </mat-list>
   </div>
-  <mat-list fxFlex="50">
-    <mat-list-item>туда</mat-list-item>
-    <mat-list-item>сюда</mat-list-item>
-  </mat-list>
-  <mat-card fxFlex="25">
-    всё обо всём
-  </mat-card>
 </div>

+ 4 - 0
src/app/src/app/person/person.page.component.scss

@@ -0,0 +1,4 @@
+.account-header-avatar {
+  background-image: url('../../assets/account.png');
+  background-size: cover;
+}

+ 92 - 5
src/app/src/app/person/person.page.component.ts

@@ -1,29 +1,116 @@
-import {Component, OnInit} from "@angular/core";
-import {AccountInfo, AccountService} from "../account/account.service";
+import {Component, Inject, OnInit} from "@angular/core";
+import {AccountEvent, AccountInfo, AccountService} from "../bank/account/account.service";
 import {PersonService} from "./person.service";
+import {HttpClient} from "@angular/common/http";
+import * as _ from 'underscore'
+import {BankService, DemurrageInfo} from "../bank/bank.service";
+import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
+import {AppComponent} from "../app.component";
 
 @Component({
   selector: 'person-page',
-  templateUrl: 'person.page.component.html'
+  templateUrl: 'person.page.component.html',
+  styleUrls: ['person.page.component.scss']
 })
 export class PersonPageComponent implements OnInit {
 
   accounts: AccountInfo[] = []
+  events: AccountEvent[] = []
+  demurrage: DemurrageInfo
 
-  constructor(private personService: PersonService, private accountService: AccountService) {
+  constructor(private app: AppComponent, private personService: PersonService, private accountService: AccountService, private bankService: BankService, private httpClient: HttpClient, private matDialog: MatDialog) {
+    app.setTitle("ЛИЧНЫЙ КАБИНЕТ")
   }
 
   ngOnInit(): void {
     this.personService.getCurrentPerson().subscribe(person => {
       this.accountService.getAccountsInfo(person.auth.id).subscribe(accounts => {
         this.accounts = accounts
+        let accountIds = accounts.map(e => e.id)
+        this.accountService.getAccountEvents(person.auth.id).subscribe(events => {
+          this.events = events.map(e => {
+            if (_.indexOf(accountIds, e.info.from) >= 0) {
+              e.info.direction = "from"
+            } else if (_.indexOf(accountIds, e.info.to) >= 0) {
+              e.info.direction = "to"
+            } else {
+              e.info.direction = "unknown"
+            }
+            return e;
+          })
+        })
       })
     })
-
+    this.bankService.getNextDemurrage().subscribe(res => this.demurrage = res)
   }
 
 
   addAccount() {
     this.personService.getCurrentPerson().subscribe(person => this.accountService.addAccount(person.auth.id).subscribe(res => this.ngOnInit()))
   }
+
+  iNeedMoney() {
+    this.personService.getCurrentPerson().subscribe(person => this.httpClient.post(`/api/old/person/${person.auth.id}/givemoney`, {}).subscribe(res => this.ngOnInit()))
+  }
+
+  getTotatOverall(): number {
+    return this.accounts.map(a => a.overall).reduce((total, x) => total + x, 0)
+  }
+
+
+  showDebitDialog(account: AccountInfo) {
+    this.personService.getCurrentPerson().subscribe(person => {
+      let ref = this.matDialog.open(PersonDebitDialogComponent, {
+        width: '480px', height: '640px', data: <PersonDebitData>{
+          accounts: this.accounts,
+          account: account,
+          personId: person.auth.id,
+          once: true,
+          time: 3
+        }
+      });
+      ref.afterClosed().subscribe((res: PersonDebitData) => {
+        if (res.action == 'create') {
+
+        }
+      })
+    })
+  }
+}
+
+@Component({
+  selector: 'person-debit-dialog',
+  templateUrl: 'person.debit.dialog.component.html',
+  styleUrls: ['person.debit.dialog.component.scss']
+})
+export class PersonDebitDialogComponent {
+
+  constructor(private httpClient: HttpClient, private dialogRef: MatDialogRef<PersonDebitDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: PersonDebitData) {
+    this.dialogRef.beforeClosed().subscribe(res => {
+
+    })
+  }
+
+  getCodeUrl(code: string): string {
+    let proto = window.location.port != "443" ? "http" : "https";
+    let port = window.location.port == "80" ? "" : window.location.port == "443" ? "" : window.location.port
+    return `${proto}://${window.location.host}${port}/c/${code}`
+  }
+
+  getCode() {
+    this.httpClient.post(`/api/old/person/${this.data.personId}/account/${this.data.account.id}/debit-by-link`, {amount: this.data.amount, once: this.data.once, time: this.data.time}).subscribe((res: any) => {
+      this.data.code = res.code
+    })
+  }
+}
+
+export class PersonDebitData {
+  account: AccountInfo
+  accounts: AccountInfo[]
+  personId: string
+  action: string
+  amount: number
+  time: number
+  once: boolean
+  code: string
 }

二進制
src/app/src/assets/account.png


+ 26 - 0
src/main/java/inn/ocsf/bee/freigeld/core/model/PublicLink.java

@@ -0,0 +1,26 @@
+package inn.ocsf.bee.freigeld.core.model;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+@Document("public-link")
+public abstract class PublicLink {
+
+    @Id
+    private String id;
+
+    public PublicLink(String id) {
+        this.id = id;
+    }
+
+    public PublicLink() {
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+}

+ 10 - 0
src/main/java/inn/ocsf/bee/freigeld/core/model/data/TicketData.java

@@ -26,6 +26,8 @@ public class TicketData {
 
     private Date ts;
 
+    private Date hold;
+
     public String getTopic() {
         return topic;
     }
@@ -65,4 +67,12 @@ public class TicketData {
     public void setTs(Date ts) {
         this.ts = ts;
     }
+
+    public Date getHold() {
+        return hold;
+    }
+
+    public void setHold(Date hold) {
+        this.hold = hold;
+    }
 }

+ 7 - 0
src/main/java/inn/ocsf/bee/freigeld/core/repo/PublicLinkRepository.java

@@ -0,0 +1,7 @@
+package inn.ocsf.bee.freigeld.core.repo;
+
+import inn.ocsf.bee.freigeld.core.model.PublicLink;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+public interface PublicLinkRepository extends MongoRepository<PublicLink, String>, PublicLinkRepositoryCustom {
+}

+ 11 - 0
src/main/java/inn/ocsf/bee/freigeld/core/repo/PublicLinkRepositoryCustom.java

@@ -0,0 +1,11 @@
+package inn.ocsf.bee.freigeld.core.repo;
+
+import inn.ocsf.bee.freigeld.core.model.PublicLink;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.function.Function;
+
+public interface PublicLinkRepositoryCustom {
+
+    PublicLink getFreeIdAndSave(@NotNull Function<String, PublicLink> fn);
+}

+ 3 - 3
src/main/java/inn/ocsf/bee/freigeld/core/repo/TicketRepository.java

@@ -6,14 +6,14 @@ import org.bson.types.ObjectId;
 import org.springframework.data.mongodb.repository.MongoRepository;
 import org.springframework.data.querydsl.QuerydslPredicateExecutor;
 
-import java.util.Collection;
+import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.stream.Stream;
 
 public interface TicketRepository extends MongoRepository<TicketData, ObjectId>, QuerydslPredicateExecutor<TicketData> {
 
-    Optional<TicketData> findFirstByTopicAndStatusInOrderByIdAsc(String topic, Collection<TicketStatus> status);
-
     Optional<TicketData> findOneByTicketId(UUID ticket_id);
 
+    Stream<TicketData> findAllByTopicAndStatusInOrderByTsDesc(String topic, List<TicketStatus> status);
 }

+ 4 - 0
src/main/java/inn/ocsf/bee/freigeld/utils/ZBase32Utils.java

@@ -10,4 +10,8 @@ public class ZBase32Utils {
     public static String encode(String str) {
         return codec.encodeAsString(str.getBytes(StandardCharsets.UTF_8));
     }
+
+    public static String decode(String str) {
+        return new String(codec.decode(str), StandardCharsets.UTF_8);
+    }
 }

+ 23 - 3
src/main/kotlin/inn/ocsf/bee/freigeld/core/data/CentralBankQueueLevel.kt

@@ -11,6 +11,7 @@ import org.springframework.stereotype.Service
 import org.springframework.transaction.annotation.Transactional
 import java.util.*
 import java.util.concurrent.CompletableFuture
+import java.util.stream.Collectors
 import javax.annotation.PostConstruct
 import javax.inject.Inject
 
@@ -187,13 +188,20 @@ class CentralBankQueueLevel : CentralBankAccountLevel() {
                 true
             }
             is BankResumeAfterRecalcEvent -> {
-                accounts.map { it to it.coins?.map { it.id }?.intersect(e.emitterEvent?.nullCoins ?: setOf()) }.filter { it.second?.isNotEmpty() ?: false }.forEach { ap ->
+                val ids = accounts.map { it to it.coins?.map { it.id }?.intersect(e.emitterEvent?.nullCoins ?: setOf()) }.filter { it.second?.isNotEmpty() ?: false }.map { ap ->
                     val nullCoins = ap.second?.map { ap.first.extractOne(it) }?.toSet() ?: setOf()
-                    emitter.accept(nullCoins)
-                }
+                    if (nullCoins.isNotEmpty()) {
+                        emitter.accept(nullCoins)
+                        ap.first
+                    } else {
+                        null
+                    }
+                }.map { it?.id }.filter { it != null }.collect(Collectors.toList()).filterNotNull().toSet()
+                ticketService.offerLast(bankTicketChannelName, BankAffectAfterRecalcEvent(UUID.randomUUID(), e.emitterEvent?.nullCoins, ids, e))
                 getCurrentState { it.queueState = BankQueueState.open }
                 true
             }
+            is BankAffectAfterRecalcEvent -> true
             else -> throw IllegalArgumentException("wrong event type ${e.javaClass.name}")
         }
     }
@@ -283,3 +291,15 @@ class BankResumeAfterRecalcEvent() : AbstractBankEvent() {
     }
 }
 
+class BankAffectAfterRecalcEvent() : AbstractBankEvent() {
+    var nullCoins: Set<UUID>? = null
+    var nullAccounts: Set<UUID>? = null
+    var parentEvent: BankResumeAfterRecalcEvent? = null
+
+    constructor(id: UUID, nullCoins: Set<UUID>?, nullAccounts: Set<UUID>?, parent: BankResumeAfterRecalcEvent) : this() {
+        this.id = id
+        this.nullAccounts = nullAccounts
+        this.nullCoins = nullCoins
+        this.parentEvent = parent
+    }
+}

+ 53 - 3
src/main/kotlin/inn/ocsf/bee/freigeld/core/data/GlobalEmitterImpl.kt

@@ -8,12 +8,19 @@ import inn.ocsf.bee.freigeld.core.model.data.CoinStatus
 import inn.ocsf.bee.freigeld.core.model.data.QCoinData.coinData
 import inn.ocsf.bee.freigeld.core.repo.CoinRepository
 import inn.ocsf.bee.freigeld.core.service.TicketService
+import inn.ocsf.bee.freigeld.utils.KeyValueBucket
+import inn.ocsf.bee.freigeld.utils.KeyValueStorage
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 import org.slf4j.LoggerFactory
+import org.springframework.scheduling.annotation.Scheduled
 import org.springframework.stereotype.Service
 import org.springframework.transaction.annotation.Transactional
+import java.time.DayOfWeek
+import java.time.LocalDate
+import java.time.ZoneId
+import java.time.temporal.TemporalAdjusters
 import java.util.*
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.locks.ReentrantLock
@@ -35,6 +42,9 @@ class GlobalEmitterImpl : GlobalEmitter {
     @Inject
     private lateinit var ticketService: TicketService
 
+    @Inject
+    private lateinit var kvStore: KeyValueStorage
+
     private val globalId = UUID.fromString("a671cdca-782a-4caf-8aad-056f6b62d822")
 
     private val listeners = mutableListOf<Consumer<EmitterEvent>>()
@@ -43,12 +53,42 @@ class GlobalEmitterImpl : GlobalEmitter {
 
     private val log = LoggerFactory.getLogger(javaClass)
 
+    @Scheduled(fixedDelay = 1000L)
+    @Transactional
+    fun initcalc() {
+        val now = LocalDate.now()
+        val mark = kvStore.get<String>(KeyValueBucket.emitterToPlan, now.year)
+        if (mark == null) {
+            val start = LocalDate.of(now.year, 1, 1)
+            val end = LocalDate.now().with(TemporalAdjusters.lastDayOfYear())
+            //val leap = now.isLeapYear
+            var dt = start
+            val planList = mutableListOf<LocalDate>()
+            while (!dt.isAfter(end)) {
+                if (dt.dayOfWeek == DayOfWeek.SATURDAY) {
+                    planList.add(dt)
+                }
+                dt = dt.plusDays(1)
+            }
+            val ids = planList.filter { it.isAfter(now) }.map { it.atStartOfDay().plusHours(3) }.map { Date.from(it.atZone(ZoneId.systemDefault()).toInstant()) }.map {
+                ticketService.offerAfter(emitterTicketChannel, it, EmitterRecalculationEvent(UUID.randomUUID(), it))
+            }.joinToString(",")
+            kvStore.put(KeyValueBucket.emitterToPlan, now.year, ids)
+        }
+    }
+
     @PostConstruct
     fun init() {
         //coinRepo.deleteAll()
-        ticketService.listen(emitterTicketChannel) {
-            calc()
-            true
+        ticketService.listen(emitterTicketChannel) { e ->
+            when (e) {
+                is EmitterRecalculationEvent -> {
+                    calc()
+                    log.info("emitter recalculated ${e.dt}")
+                    true
+                }
+                else -> throw RuntimeException("wrong event type ${e::class.java}")
+            }
         }
     }
 
@@ -160,4 +200,14 @@ class GlobalEmitterImpl : GlobalEmitter {
     override fun extractOne(coinId: UUID): Coin? {
         return setStatus(coinId, CoinStatus.outdoor, setOf(CoinStatus.indoor)).coin
     }
+}
+
+class EmitterRecalculationEvent() : EmitterEvent() {
+
+    var dt: Date? = null
+
+    constructor(eventId: UUID, dt: Date) : this() {
+        this.id = eventId
+        this.dt = dt
+    }
 }

+ 5 - 3
src/main/kotlin/inn/ocsf/bee/freigeld/core/demo/DemoInMem.kt

@@ -1,11 +1,13 @@
 package inn.ocsf.bee.freigeld.core.demo
 
-import inn.ocsf.bee.freigeld.core.model.*
+import inn.ocsf.bee.freigeld.core.model.BankAccount
+import inn.ocsf.bee.freigeld.core.model.CentralBank
+import inn.ocsf.bee.freigeld.core.model.GlobalEmitter
+import inn.ocsf.bee.freigeld.core.model.GlobalWorld
 import inn.ocsf.bee.freigeld.core.service.TicketService
 import org.slf4j.LoggerFactory
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.stereotype.Service
-import java.util.*
 import javax.annotation.PostConstruct
 import javax.inject.Inject
 
@@ -82,7 +84,7 @@ class DemoInMem {
 
     //@Scheduled(initialDelay = 60 * 1000L, fixedDelay = 60 * 1000L)
     fun calc() {
-        ticketService.offerLast("${GlobalEmitter::class.java}", object : Ticket(UUID.randomUUID()) {})
+        //ticketService.offerAfter(GlobalEmitterImpl.emitterTicketChannel, Date.from(LocalDateTime.now().plusMinutes(5).atZone(ZoneId.systemDefault()).toInstant()), object : Ticket(UUID.randomUUID()) {})
         log.info("calc")
     }
 }

+ 1 - 1
src/main/kotlin/inn/ocsf/bee/freigeld/core/repo/CoinRepositoryImpl.kt

@@ -8,7 +8,7 @@ import org.springframework.data.mongodb.core.query.Criteria
 import org.springframework.data.mongodb.core.query.Query
 import javax.inject.Inject
 
-class CoinRepositoryImpl : CoinRepositoryCustom {
+open class CoinRepositoryImpl : CoinRepositoryCustom {
 
     @Inject
     private lateinit var mongo: MongoOperations

+ 40 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/core/repo/PublicLinkRepositoryImpl.kt

@@ -0,0 +1,40 @@
+package inn.ocsf.bee.freigeld.core.repo
+
+import inn.ocsf.bee.freigeld.core.model.PublicLink
+import inn.ocsf.bee.freigeld.utils.ZBase32Utils
+import org.apache.commons.lang3.RandomStringUtils
+import org.springframework.context.annotation.Lazy
+import org.springframework.transaction.annotation.Transactional
+import java.util.function.Function
+import javax.inject.Inject
+
+open class PublicLinkRepositoryImpl : PublicLinkRepositoryCustom {
+
+    @Inject
+    @Lazy
+    private lateinit var linkRepo: PublicLinkRepository
+
+    private fun getIds(): Set<String> {
+        return (2..5).flatMap { l ->
+            (1..5).map { n ->
+                val str = RandomStringUtils.randomAlphanumeric(l)
+                ZBase32Utils.encode(str).trim('=')
+            }
+        }.toSet()
+    }
+
+    @Transactional
+    override fun getFreeIdAndSave(fn: Function<String, PublicLink?>): PublicLink? {
+        var ret: PublicLink? = null
+        while (ret == null) {
+            val id = getIds().filter { !linkRepo.existsById(it) }.firstOrNull()
+            if (id != null) {
+                val ret0 = fn.apply(id)
+                if (ret0 == null) throw RuntimeException("should not be null")
+                if (ret0.id != id) throw RuntimeException("wrong id")
+                ret = linkRepo.save(ret0)
+            }
+        }
+        return ret
+    }
+}

+ 20 - 3
src/main/kotlin/inn/ocsf/bee/freigeld/core/service/TicketService.kt

@@ -8,11 +8,13 @@ import inn.ocsf.bee.freigeld.core.repo.TicketRepository
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import org.bson.types.ObjectId
 import org.slf4j.LoggerFactory
 import org.springframework.stereotype.Service
 import java.util.*
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.CopyOnWriteArrayList
+import java.util.stream.Stream
 import javax.annotation.PostConstruct
 import javax.inject.Inject
 
@@ -29,6 +31,10 @@ class TicketService {
 
     private val log = LoggerFactory.getLogger(javaClass)
 
+    fun findAll(topic: String, statusSet: Set<TicketStatus>): Stream<TicketData> {
+        return ticketRepo.findAllByTopicAndStatusInOrderByTsDesc(topic, statusSet.toList())
+    }
+
     private fun tick() {
         listenerMap.keys().iterator().forEach { topic ->
             peekFirst(topic)?.let { t ->
@@ -66,17 +72,28 @@ class TicketService {
         ticketRepo.save(td)
     }
 
-    fun offerLast(topic: String, e: Ticket) {
+    private fun newTicket(topic: String, e: Ticket): TicketData {
         val td = TicketData()
         td.status = TicketStatus.created
         td.ts = Date()
         td.topic = topic
         td.ticket = e
-        ticketRepo.save(td)
+        return td
+    }
+
+    fun offerAfter(topic: String, after: Date, e: Ticket): ObjectId {
+        val td = newTicket(topic, e)
+        td.hold = after
+        return ticketRepo.save(td).id
+    }
+
+    fun offerLast(topic: String, e: Ticket): ObjectId {
+        return ticketRepo.save(newTicket(topic, e)).id
     }
 
     fun peekFirst(topic: String, modifyFn: ((Ticket) -> Ticket)? = null): Ticket? {
-        return ticketRepo.findFirstByTopicAndStatusInOrderByIdAsc(topic, listOf(TicketStatus.created)).orElse(null)?.let {
+        val dt = Date()
+        return findAll(topic, setOf(TicketStatus.created)).filter { it.hold == null || it.hold.before(dt) || it.hold.equals(dt) }.limit(1).findFirst().orElse(null)?.let {
             if (modifyFn != null) {
                 it.ticket = modifyFn(it.ticket)
                 ticketRepo.save(it)

+ 27 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/serve/rest/BankController.kt

@@ -0,0 +1,27 @@
+package inn.ocsf.bee.freigeld.serve.rest
+
+import inn.ocsf.bee.freigeld.core.data.EmitterRecalculationEvent
+import inn.ocsf.bee.freigeld.core.data.GlobalEmitterImpl
+import inn.ocsf.bee.freigeld.core.model.TicketStatus
+import inn.ocsf.bee.freigeld.core.service.TicketService
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.RequestMapping
+import org.springframework.web.bind.annotation.RequestMethod
+import org.springframework.web.bind.annotation.RestController
+import javax.inject.Inject
+import kotlin.streams.toList
+
+@RestController
+class BankController {
+
+    @Inject
+    private lateinit var ticketService: TicketService
+
+    @RequestMapping("api/bank/demurrage/next", method = [RequestMethod.GET])
+    fun getNextDemurrageInfo(): ResponseEntity<List<DemurrageInfo>> {
+        return ResponseEntity.ok(ticketService.findAll(GlobalEmitterImpl.emitterTicketChannel, setOf(TicketStatus.created)).map { it.ticket }.filter { it is EmitterRecalculationEvent }.map { it as EmitterRecalculationEvent }.toList().sortedBy { it.dt }.map { DemurrageInfo(it.dt?.time ?: 0L) })
+    }
+
+}
+
+data class DemurrageInfo(val dt: Long)

+ 81 - 4
src/main/kotlin/inn/ocsf/bee/freigeld/serve/rest/PersonController.kt

@@ -12,21 +12,27 @@ import dev.samstevens.totp.secret.DefaultSecretGenerator
 import dev.samstevens.totp.time.SystemTimeProvider
 import dev.samstevens.totp.time.TimeProvider
 import dev.samstevens.totp.util.Utils.getDataUriForImage
-import inn.ocsf.bee.freigeld.core.model.CentralBank
-import inn.ocsf.bee.freigeld.core.model.GlobalWorld
-import inn.ocsf.bee.freigeld.core.model.PersonIdentityFullName
-import inn.ocsf.bee.freigeld.core.model.PersonIdentitySecret
+import inn.ocsf.bee.freigeld.core.data.CentralBankQueueLevel
+import inn.ocsf.bee.freigeld.core.data.ExchangeFailedEvent
+import inn.ocsf.bee.freigeld.core.data.ExchangeSuccessEvent
+import inn.ocsf.bee.freigeld.core.demo.getSelfAccount
+import inn.ocsf.bee.freigeld.core.model.*
 import inn.ocsf.bee.freigeld.core.model.data.PersonData
 import inn.ocsf.bee.freigeld.core.repo.PersonRepository
+import inn.ocsf.bee.freigeld.core.repo.PublicLinkRepository
+import inn.ocsf.bee.freigeld.core.service.TicketService
 import inn.ocsf.bee.freigeld.serve.JwtAuthenticationController
 import inn.ocsf.bee.freigeld.utils.KeyValueBucket.personToSite
 import inn.ocsf.bee.freigeld.utils.KeyValueBucket.secretToSite
 import inn.ocsf.bee.freigeld.utils.KeyValueStorage
 import org.apache.commons.lang3.RandomStringUtils
+import org.slf4j.LoggerFactory
+import org.springframework.context.annotation.Profile
 import org.springframework.http.ResponseEntity
 import org.springframework.web.bind.annotation.*
 import java.util.*
 import javax.inject.Inject
+import kotlin.streams.toList
 
 @RestController
 class PersonController {
@@ -37,21 +43,92 @@ class PersonController {
     @Inject
     private lateinit var bank: CentralBank
 
+    @Inject
+    private lateinit var ticketService: TicketService
+
+    @Inject
+    private lateinit var linkRepo: PublicLinkRepository
+
+    private val log = LoggerFactory.getLogger(javaClass)
+
+
+    @RequestMapping("api/old/person/{personId}/account/{accountId}/debit-by-link", method = [RequestMethod.POST])
+    fun getDebitLink(@PathVariable("personId") personId: UUID, @PathVariable("accountId") accountId: UUID, @RequestBody lreq: DebitLinkReq): ResponseEntity<Map<String, Any>> {
+        val link = linkRepo.getFreeIdAndSave { id -> PublicLinkDebitAccount(id, personId, accountId, lreq.amount, lreq.once, lreq.time) }
+        return ResponseEntity.ok(mapOf("code" to link.id)) //p7do
+    }
+
     @RequestMapping("api/old/person/{personId}/accounts", method = [RequestMethod.GET])
     fun getAccountsInfo(@PathVariable("personId") personId: UUID): ResponseEntity<Map<String, Any?>> {
         val accounts = bank.getAccounts(personId).map { AccountInfo(it.id, it.overall) }
         return ResponseEntity.ok(mapOf("accounts" to accounts))
     }
 
+    @RequestMapping("api/old/person/{personId}/events", method = [RequestMethod.GET])
+    fun getEvents(@PathVariable("personId") personId: UUID, @RequestParam("limit", defaultValue = "25") limit: Long, @RequestParam("after", required = false) eventId: UUID?): ResponseEntity<List<AccountEvent>> {
+        val accounts = bank.getAccounts(personId).map { it.id }
+        var skipping = eventId != null
+        val events = ticketService.findAll(CentralBankQueueLevel.bankTicketChannelName, setOf(TicketStatus.completed)).filter {
+            val skippingThis = skipping
+            if (skipping) if (it.ticket.id == eventId) skipping = false
+            !skippingThis
+        }.filter {
+            val ticket = it.ticket
+            val ids = when (ticket) {
+                is ExchangeFailedEvent -> setOf(ticket.parentEvent?.from, ticket.parentEvent?.to)
+                is ExchangeSuccessEvent -> setOf(ticket.parentEvent?.from, ticket.parentEvent?.to)
+                else -> setOf()
+            }
+            accounts.intersect(ids).isNotEmpty()
+        }.map {
+            val ticket = it.ticket as BankEvent
+            AccountEvent(it.ticket.id, it.ticket::class.java.typeName, it.ts.time, ticket)
+        }.limit(limit).toList().sortedByDescending { it.ts }
+        return ResponseEntity.ok(events)
+    }
+
     @RequestMapping("api/old/person/{personId}/accounts/add", method = [RequestMethod.POST])
     fun addAccount(@PathVariable("personId") personId: UUID): ResponseEntity<Map<String, Any?>> {
         val account = bank.addAccount(personId)
         return ResponseEntity.ok(mapOf("id" to account.id))
     }
+
+
+    @Profile("dev")
+    @RequestMapping("api/old/person/{personId}/givemoney", method = [RequestMethod.POST])
+    fun giveMoney(@PathVariable("personId") personId: UUID): ResponseEntity<Map<String, Any?>> {
+        val account = bank.getAccounts(personId).random()
+        bank.exchange(account, bank.getSelfAccount(), (100 * Math.random()).toLong()).get()
+        return ResponseEntity.ok(mapOf())
+    }
 }
 
+data class DebitLinkReq(val amount: Long?, val once: Boolean?, val time: Long?)
+
 data class AccountInfo(val id: UUID, val overall: Long)
 
+data class AccountEvent(val id: UUID, val type: String, val ts: Long, val info: BankEvent)
+
+class PublicLinkDebitAccount() : PublicLink() {
+
+    var personId: UUID? = null
+    var accountId: UUID? = null
+    var amount: Long? = null
+    var once: Boolean? = null
+    var time: Long? = null
+    var dt: Date? = null
+
+    constructor(id: String, personId: UUID, accountId: UUID, amount: Long?, once: Boolean?, time: Long?) : this() {
+        this.id = id
+        this.personId = personId
+        this.accountId = accountId
+        this.amount = amount
+        this.once = once
+        this.time = time
+        this.dt = Date()
+    }
+}
+
 @RestController
 @CrossOrigin
 class PersonAuthController : JwtAuthenticationController() {

+ 1 - 0
src/main/kotlin/inn/ocsf/bee/freigeld/utils/KeyValueStorage.kt

@@ -33,6 +33,7 @@ class KeyValueStorage {
 
 enum class KeyValueBucket(val bucket: String) {
     personToSite("person-to-site"),
+    emitterToPlan("emitter-to-plan"),
     secretToSite("secret-to-site");
 }