Переглянути джерело

первая передача денег!

kpmy 5 роки тому
батько
коміт
8afcda76fb

+ 3 - 3
src/app/src/app/app.module.ts

@@ -21,7 +21,7 @@ import {MatIconModule} from "@angular/material/icon";
 import {PersonService} from "./person/person.service";
 import {JwtModule} from "@auth0/angular-jwt";
 import {NgxInitModule} from "ngx-init";
-import {PersonDebitDialogComponent, PersonPageComponent} from "./person/person.page.component";
+import {PersonCreditDialogComponent, PersonDebitDialogComponent, PersonPageComponent} from "./person/person.page.component";
 import {MatListModule} from "@angular/material/list";
 import {AccountService} from "./bank/account/account.service";
 import {MatProgressBarModule} from "@angular/material/progress-bar";
@@ -44,10 +44,10 @@ export function jwtTokenGetter() {
 
 @NgModule({
   declarations: [
-    AppComponent, AppLoginComponent, AppIndexComponent, PersonPageComponent, PersonDebitDialogComponent, PendingSnackBar
+    AppComponent, AppLoginComponent, AppIndexComponent, PersonPageComponent, PersonDebitDialogComponent, PendingSnackBar, PersonCreditDialogComponent
   ],
   entryComponents: [
-    PersonDebitDialogComponent, PendingSnackBar
+    PersonDebitDialogComponent, PendingSnackBar, PersonCreditDialogComponent
   ],
   imports: [
     BrowserModule,

+ 34 - 0
src/app/src/app/person/person.credit.dialog.component.html

@@ -0,0 +1,34 @@
+<h2 mat-dialog-title>ОТПРАВИТЬ ДЕНЬГИ</h2>
+<div class="person-credit-dialog-content" fxLayout="column" fxLayoutAlign="start stretch" fxLayoutGap="0.5em" mat-dialog-content>
+  <div fxLayout="row" fxLayoutAlign="start center">
+    <mat-form-field>
+      <mat-label id="credit-dialog-code-label">Код пополнения</mat-label>
+      <input (ngModelChange)="checkCode($event)" [ngModel]="data.code" aria-labelledby="credit-dialog-code-label" matInput/>
+    </mat-form-field>
+    <mat-icon *ngIf="codeOk === 1">done</mat-icon>
+    <mat-icon *ngIf="codeOk === 0">not_interested</mat-icon>
+  </div>
+  <mat-form-field>
+    <mat-label>Сумма</mat-label>
+    <input [(ngModel)]="data.amount" [max]="data.account.overall" matInput min="1" step="1" type="number"/>
+  </mat-form-field>
+  <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>
+  <div fxLayout="row" fxLayoutAlign="start center">
+    <mat-form-field>
+      <mat-label id="credit-dialog-key-label">Шестизначный код</mat-label>
+      <input (ngModelChange)="checkKey($event)" [ngModel]="key" aria-labelledby="credit-dialog-key-label" matInput/>
+      <mat-hint>Введите код из приложения Authenticator, когда будете готовы совершить платёж</mat-hint>
+    </mat-form-field>
+    <mat-icon *ngIf="keyOk === 1">done</mat-icon>
+    <mat-icon *ngIf="keyOk === 0">not_interested</mat-icon>
+  </div>
+</div>
+<div fxLayout="row" fxLayoutAlign="end center" mat-dialog-actions>
+  <button [disabled]="keyOk != 1 || codeOk != 1 || data.account.overall < data.amount" [mat-dialog-close]="{action: 'create', key: key, data: data}" color="warn" mat-button>ОТПРАВИТЬ ДЕНЬГИ</button>
+  <button [mat-dialog-close]="{data: data}" mat-button>ОТМЕНА</button>
+</div>

+ 3 - 0
src/app/src/app/person/person.credit.dialog.component.scss

@@ -0,0 +1,3 @@
+.person-credit-dialog-content {
+  overflow: hidden;
+}

+ 5 - 2
src/app/src/app/person/person.debit.dialog.component.html

@@ -14,9 +14,12 @@
   <div style="padding-top: 1em">
     <mat-checkbox [(ngModel)]="data.once" labelPosition="after">сделать ссылку одноразовой</mat-checkbox>
   </div>
+  <div style="padding-top: 1em">
+    <mat-checkbox [(ngModel)]="data.limited" 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-label id="time-radio-label">Время действия</mat-label>
+    <mat-radio-group [(ngModel)]="data.time" [disabled]="!data.limited" 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>

+ 8 - 3
src/app/src/app/person/person.page.component.html

@@ -33,9 +33,14 @@
         <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 fxLayout="row" fxLayoutAlign="start center">
+            <button (click)="showDebitDialog(account)" [disabled]="!hovered[idx]" class="mat-elevation-z0" color="warn" mat-button matTooltip="ПОПОЛНИТЬ">
+              <mat-icon>archive</mat-icon>
+            </button>
+            <button (click)="showCreditDialog(account)" [disabled]="!hovered[idx]" class="mat-elevation-z0" color="primary" mat-button matTooltip="ОПЛАТИТЬ">
+              <mat-icon>unarchive</mat-icon>
+            </button>
+          </div>
         </div>
       </mat-list-item>
     </mat-list>

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

@@ -69,12 +69,30 @@ export class PersonPageComponent implements OnInit {
           time: 3
         }
       });
-      ref.afterClosed().subscribe((res: PersonDebitData) => {
-        if (res.action == 'create') {
+      ref.afterClosed().subscribe((res: any) => {
+        //do nothing
+      })
+    })
+  }
 
+  showCreditDialog(account: AccountInfo) {
+    this.personService.getCurrentPerson().subscribe(person => {
+      let ref = this.matDialog.open(PersonCreditDialogComponent, {
+        width: '480px', height: '640px', data: <PersonCreditData>{
+          accounts: this.accounts,
+          account: account,
+          personId: person.auth.id
         }
       })
-    })
+      ref.afterClosed().subscribe((res: any) => {
+        if (res.action == 'create') {
+          let data = <PersonCreditData>res.data
+          this.httpClient.post(`/api/old/person/${person.auth.id}/account/${data.account.id}/credit/by-link/${data.code}`, {amount: data.amount}).subscribe((res: any) => {
+            console.log(`transaction registered ${res.id}`)
+          })
+        }
+      })
+    });
   }
 }
 
@@ -104,13 +122,67 @@ export class PersonDebitDialogComponent {
   }
 }
 
-export class PersonDebitData {
+abstract class PersonAccountData {
   account: AccountInfo
   accounts: AccountInfo[]
   personId: string
-  action: string
+}
+
+export class PersonCreditData extends PersonAccountData {
+  code: string
+  amount: number
+}
+
+export class PersonDebitData extends PersonAccountData {
   amount: number
   time: number
   once: boolean
+  limited: boolean
   code: string
 }
+
+@Component({
+  selector: 'person-credit-dialog',
+  templateUrl: 'person.credit.dialog.component.html',
+  styleUrls: ['person.credit.dialog.component.scss']
+})
+export class PersonCreditDialogComponent {
+
+  key: string
+  keyOk: number
+  codeOk: number
+
+  constructor(private httpClient: HttpClient, private dialogRef: MatDialogRef<PersonDebitDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: PersonCreditData) {
+    this.dialogRef.beforeClosed().subscribe(res => {
+
+    })
+  }
+
+  checkCode(x: string) {
+    this.httpClient.get(`/api/old/person/${this.data.personId}/get-credit-link/${x}`).subscribe((res: any) => {
+      if (res.ok) {
+        this.codeOk = 1
+        if (res.amount > 0) this.data.amount = res.amount
+      } else {
+        this.codeOk = 0
+      }
+      this.data.code = x;
+    }, error => {
+      this.codeOk = 0
+    })
+  }
+
+  checkKey(x: string) {
+    this.httpClient.post(`/api/old/person/${this.data.personId}/check6`, {key: x}).subscribe((res: any) => {
+      if (res.ok) {
+        this.keyOk = 1
+      } else {
+        this.keyOk = 0
+      }
+      this.key = x
+    }, error => {
+      this.keyOk = 0
+    });
+  }
+
+}

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

@@ -1,6 +1,7 @@
 package inn.ocsf.bee.freigeld.core.model;
 
 import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Version;
 import org.springframework.data.mongodb.core.mapping.Document;
 
 @Document("public-link")
@@ -9,6 +10,9 @@ public abstract class PublicLink {
     @Id
     private String id;
 
+    @Version
+    private Long version;
+
     public PublicLink(String id) {
         this.id = id;
     }

+ 84 - 10
src/main/kotlin/inn/ocsf/bee/freigeld/serve/rest/PersonController.kt

@@ -1,10 +1,7 @@
 package inn.ocsf.bee.freigeld.serve.rest
 
 import com.oblac.nomen.Nomen
-import dev.samstevens.totp.code.CodeGenerator
-import dev.samstevens.totp.code.DefaultCodeGenerator
-import dev.samstevens.totp.code.DefaultCodeVerifier
-import dev.samstevens.totp.code.HashingAlgorithm
+import dev.samstevens.totp.code.*
 import dev.samstevens.totp.qr.QrData
 import dev.samstevens.totp.qr.ZxingPngQrGenerator
 import dev.samstevens.totp.recovery.RecoveryCodeGenerator
@@ -28,9 +25,13 @@ 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.HttpStatus
 import org.springframework.http.ResponseEntity
 import org.springframework.web.bind.annotation.*
+import java.time.LocalDateTime
+import java.time.ZoneId
 import java.util.*
+import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 import kotlin.streams.toList
 
@@ -52,9 +53,24 @@ class PersonController {
     private val log = LoggerFactory.getLogger(javaClass)
 
 
+    @RequestMapping("/api/old/person/{personId}/get-credit-link/{linkId}", method = [RequestMethod.GET])
+    fun getCreditLink(@PathVariable("personId") personId: UUID, @PathVariable("linkId") linkId: String): ResponseEntity<Map<String, Any?>> {
+        val link = linkRepo.findById(linkId).filter { it is PublicLinkDebitAccount }.map { it as PublicLinkDebitAccount }.orElse(null)
+        if (link != null) {
+            val dt = LocalDateTime.from((link.dt ?: Date()).toInstant().atZone(ZoneId.systemDefault()))
+            if (link.limited != true || !(dt.plusHours(link.time!!).isBefore(LocalDateTime.now()))) {
+                return ResponseEntity.ok(mapOf("ok" to true, "amount" to link.amount))
+            } else {
+                return ResponseEntity(HttpStatus.GONE)
+            }
+        } else {
+            return ResponseEntity(HttpStatus.NOT_FOUND)
+        }
+    }
+
     @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) }
+        val link = linkRepo.getFreeIdAndSave { id -> PublicLinkDebitAccount(id, personId, accountId, lreq.amount, lreq.once, lreq.limited, lreq.time) }
         return ResponseEntity.ok(mapOf("code" to link.id)) //p7do
     }
 
@@ -98,12 +114,43 @@ class PersonController {
     @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()
+        bank.exchange(account, bank.getSelfAccount(), (100 * Math.random()).toLong()).get(5, TimeUnit.SECONDS)
         return ResponseEntity.ok(mapOf())
     }
+
+    @RequestMapping("api/old/person/{personId}/account/{accountId}/credit/by-link/{linkId}")
+    fun creditMoney(@PathVariable("personId") personId: UUID, @PathVariable("accountId") accountId: UUID, @PathVariable("linkId") linkId: String, @RequestBody req: CreditLinkReq): ResponseEntity<Map<String, Any?>> {
+        val fromAccount = bank.getAccounts(personId).first { it.id == accountId }
+        val link = linkRepo.findById(linkId).filter { it is PublicLinkDebitAccount }.map { it as PublicLinkDebitAccount }.orElse(null)
+        val ret = if (fromAccount != null) {
+            if (link != null) {
+                val toAccount = bank.getAccount(link.accountId!!)
+                if (toAccount != null) {
+                    if (req.amount != null && req.amount > 0) {
+                        bank.exchange(toAccount, fromAccount, req.amount).get()
+                    } else {
+                        null
+                    }
+                } else {
+                    null
+                }
+            } else {
+                null
+            }
+        } else {
+            null
+        }
+        if (ret != null) {
+            return ResponseEntity.ok(mapOf("id" to ret.id))
+        } else {
+            return ResponseEntity(HttpStatus.NOT_FOUND)
+        }
+    }
 }
 
-data class DebitLinkReq(val amount: Long?, val once: Boolean?, val time: Long?)
+data class CreditLinkReq(val amount: Long?)
+
+data class DebitLinkReq(val amount: Long?, val once: Boolean?, val limited: Boolean?, val time: Long?)
 
 data class AccountInfo(val id: UUID, val overall: Long)
 
@@ -116,14 +163,16 @@ class PublicLinkDebitAccount() : PublicLink() {
     var amount: Long? = null
     var once: Boolean? = null
     var time: Long? = null
+    var limited: Boolean? = null
     var dt: Date? = null
 
-    constructor(id: String, personId: UUID, accountId: UUID, amount: Long?, once: Boolean?, time: Long?) : this() {
+    constructor(id: String, personId: UUID, accountId: UUID, amount: Long?, once: Boolean?, limited: Boolean?, time: Long?) : this() {
         this.id = id
         this.personId = personId
         this.accountId = accountId
         this.amount = amount
         this.once = once
+        this.limited = limited
         this.time = time
         this.dt = Date()
     }
@@ -159,13 +208,34 @@ class PersonAuthController : JwtAuthenticationController() {
         return ResponseEntity.ok(QrDataResponse(secret, dataUri))
     }
 
-    @RequestMapping("api/new/person/check6", method = [RequestMethod.POST])
-    fun checkqr(@RequestBody req: QrSixRequest): ResponseEntity<QrSixResponse> {
+    private fun getVerifier(): CodeVerifier {
         val timeProvider: TimeProvider = SystemTimeProvider()
         val codeGenerator: CodeGenerator = DefaultCodeGenerator(HashingAlgorithm.SHA1)
         val verifier = DefaultCodeVerifier(codeGenerator, timeProvider)
         verifier.setTimePeriod(30)
         verifier.setAllowedTimePeriodDiscrepancy(4)
+        return verifier
+    }
+
+    @RequestMapping("api/old/person/{personId}/check6", method = [RequestMethod.POST])
+    fun checkqrnow(@PathVariable("personId") personId: UUID, @RequestBody rq: QrOldSixRequest): ResponseEntity<QrOldSixResponse> {
+        val verifier = getVerifier()
+        val person = world.getPerson(personId)
+        if (person != null) {
+            val secret = world.getPersonIdentitySet(person)?.filter { it is PersonIdentitySecret }?.map { it as PersonIdentitySecret }?.firstOrNull()
+            if (secret != null) {
+                return ResponseEntity.ok(QrOldSixResponse(verifier.isValidCode(secret.key, rq.key)))
+            } else {
+                throw RuntimeException("this should not happen")
+            }
+        } else {
+            return ResponseEntity(HttpStatus.NOT_FOUND)
+        }
+    }
+
+    @RequestMapping("api/new/person/check6", method = [RequestMethod.POST])
+    fun checkqr(@RequestBody req: QrSixRequest): ResponseEntity<QrSixResponse> {
+        val verifier = getVerifier()
 
         var register = false
 
@@ -195,4 +265,8 @@ data class QrDataResponse(val key: String, val img: String)
 
 data class QrSixRequest(val siteId: UUID, val code: String, val key: String)
 
+data class QrOldSixRequest(val key: String)
+
+data class QrOldSixResponse(val ok: Boolean)
+
 data class QrSixResponse(val ok: Boolean, val register: Boolean, val token: String?)