Added users support, made CRUD for users. Updated receipts feature

This commit is contained in:
2026-03-06 23:30:58 +03:00
parent 2ee1837fcc
commit a70527b388
32 changed files with 1667 additions and 123 deletions
+622
View File
@@ -0,0 +1,622 @@
<mxfile host="Electron" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/29.5.2 Chrome/142.0.7444.265 Electron/39.6.1 Safari/537.36" version="29.5.2">
<diagram name="Страница-1" id="0m6B3G-Z3EdFeOiLiUiD">
<mxGraphModel dx="1357" dy="1036" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="JbCyL7nHgy0AnRVCQc6m-27" parent="1" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;strokeColor=default;fontSize=16;" value="Users" vertex="1">
<mxGeometry height="238" width="320" x="40" y="40" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-28" parent="JbCyL7nHgy0AnRVCQc6m-27" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="30" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-29" parent="JbCyL7nHgy0AnRVCQc6m-28" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="ID" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-30" parent="JbCyL7nHgy0AnRVCQc6m-28" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigSerial" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-31" parent="JbCyL7nHgy0AnRVCQc6m-28" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="PK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-32" parent="JbCyL7nHgy0AnRVCQc6m-27" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="56" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-33" parent="JbCyL7nHgy0AnRVCQc6m-32" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="TelegramID" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-34" parent="JbCyL7nHgy0AnRVCQc6m-32" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigInt" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-35" parent="JbCyL7nHgy0AnRVCQc6m-32" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-36" parent="JbCyL7nHgy0AnRVCQc6m-27" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="82" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-37" parent="JbCyL7nHgy0AnRVCQc6m-36" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="UserName" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-38" parent="JbCyL7nHgy0AnRVCQc6m-36" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="String" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-39" parent="JbCyL7nHgy0AnRVCQc6m-36" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-40" parent="JbCyL7nHgy0AnRVCQc6m-27" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="108" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-41" parent="JbCyL7nHgy0AnRVCQc6m-40" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="FirstName" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-42" parent="JbCyL7nHgy0AnRVCQc6m-40" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="String" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-43" parent="JbCyL7nHgy0AnRVCQc6m-40" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-44" parent="JbCyL7nHgy0AnRVCQc6m-27" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="134" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-45" parent="JbCyL7nHgy0AnRVCQc6m-44" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="LastName" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-46" parent="JbCyL7nHgy0AnRVCQc6m-44" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="String" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-47" parent="JbCyL7nHgy0AnRVCQc6m-44" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-57" parent="JbCyL7nHgy0AnRVCQc6m-27" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="160" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-58" parent="JbCyL7nHgy0AnRVCQc6m-57" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="LanguageCode" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-59" parent="JbCyL7nHgy0AnRVCQc6m-57" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="String" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-60" parent="JbCyL7nHgy0AnRVCQc6m-57" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-53" parent="JbCyL7nHgy0AnRVCQc6m-27" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="186" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-54" parent="JbCyL7nHgy0AnRVCQc6m-53" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="CreatedAt" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-55" parent="JbCyL7nHgy0AnRVCQc6m-53" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="TimeStamp" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-56" parent="JbCyL7nHgy0AnRVCQc6m-53" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-49" parent="JbCyL7nHgy0AnRVCQc6m-27" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="212" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-50" parent="JbCyL7nHgy0AnRVCQc6m-49" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="UpdatedAt" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-51" parent="JbCyL7nHgy0AnRVCQc6m-49" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="TimeStamp" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-52" parent="JbCyL7nHgy0AnRVCQc6m-49" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-61" parent="1" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;strokeColor=default;fontSize=16;gradientColor=none;fillColor=default;" value="Families" vertex="1">
<mxGeometry height="238" width="320" x="480" y="40" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-62" parent="JbCyL7nHgy0AnRVCQc6m-61" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="30" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-63" parent="JbCyL7nHgy0AnRVCQc6m-62" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="ID" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-64" parent="JbCyL7nHgy0AnRVCQc6m-62" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigSerial" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-65" parent="JbCyL7nHgy0AnRVCQc6m-62" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="PK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-152" parent="JbCyL7nHgy0AnRVCQc6m-61" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="56" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-153" parent="JbCyL7nHgy0AnRVCQc6m-152" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="OwnerId" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-154" parent="JbCyL7nHgy0AnRVCQc6m-152" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigInt" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-155" parent="JbCyL7nHgy0AnRVCQc6m-152" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="FK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-66" parent="JbCyL7nHgy0AnRVCQc6m-61" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="82" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-67" parent="JbCyL7nHgy0AnRVCQc6m-66" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="TelegramChatID" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-68" parent="JbCyL7nHgy0AnRVCQc6m-66" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigInt" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-69" parent="JbCyL7nHgy0AnRVCQc6m-66" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-148" parent="JbCyL7nHgy0AnRVCQc6m-61" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="108" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-149" parent="JbCyL7nHgy0AnRVCQc6m-148" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="TelegramChatName" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-150" parent="JbCyL7nHgy0AnRVCQc6m-148" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="String" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-151" parent="JbCyL7nHgy0AnRVCQc6m-148" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-144" parent="JbCyL7nHgy0AnRVCQc6m-61" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="134" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-145" parent="JbCyL7nHgy0AnRVCQc6m-144" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-146" parent="JbCyL7nHgy0AnRVCQc6m-144" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-147" parent="JbCyL7nHgy0AnRVCQc6m-144" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-140" parent="JbCyL7nHgy0AnRVCQc6m-61" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="160" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-141" parent="JbCyL7nHgy0AnRVCQc6m-140" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-142" parent="JbCyL7nHgy0AnRVCQc6m-140" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-143" parent="JbCyL7nHgy0AnRVCQc6m-140" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-86" parent="JbCyL7nHgy0AnRVCQc6m-61" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="186" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-87" parent="JbCyL7nHgy0AnRVCQc6m-86" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="CreatedAt" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-88" parent="JbCyL7nHgy0AnRVCQc6m-86" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="TimeStamp" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-89" parent="JbCyL7nHgy0AnRVCQc6m-86" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-90" parent="JbCyL7nHgy0AnRVCQc6m-61" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="212" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-91" parent="JbCyL7nHgy0AnRVCQc6m-90" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="UpdatedAt" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-92" parent="JbCyL7nHgy0AnRVCQc6m-90" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="TimeStamp" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-93" parent="JbCyL7nHgy0AnRVCQc6m-90" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-95" edge="1" parent="1" source="JbCyL7nHgy0AnRVCQc6m-70" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="JbCyL7nHgy0AnRVCQc6m-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="440" y="135" />
<mxPoint x="440" y="83" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-96" parent="1" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;strokeColor=default;fontSize=16;gradientColor=none;fillColor=default;" value="FamilyMembers" vertex="1">
<mxGeometry height="160" width="320" x="480" y="360" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-97" parent="JbCyL7nHgy0AnRVCQc6m-96" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="30" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-98" parent="JbCyL7nHgy0AnRVCQc6m-97" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="ID" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-99" parent="JbCyL7nHgy0AnRVCQc6m-97" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigSerial" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-100" parent="JbCyL7nHgy0AnRVCQc6m-97" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="PK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-101" parent="JbCyL7nHgy0AnRVCQc6m-96" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="56" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-102" parent="JbCyL7nHgy0AnRVCQc6m-101" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="FamilyId" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-103" parent="JbCyL7nHgy0AnRVCQc6m-101" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigInt" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-104" parent="JbCyL7nHgy0AnRVCQc6m-101" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="FK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-105" parent="JbCyL7nHgy0AnRVCQc6m-96" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="82" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-106" parent="JbCyL7nHgy0AnRVCQc6m-105" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="UserId" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-107" parent="JbCyL7nHgy0AnRVCQc6m-105" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="Bigint" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-108" parent="JbCyL7nHgy0AnRVCQc6m-105" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="FK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-109" parent="JbCyL7nHgy0AnRVCQc6m-96" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="108" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-110" parent="JbCyL7nHgy0AnRVCQc6m-109" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="JoinedAt" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-111" parent="JbCyL7nHgy0AnRVCQc6m-109" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="TimeStamp" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-112" parent="JbCyL7nHgy0AnRVCQc6m-109" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-136" parent="JbCyL7nHgy0AnRVCQc6m-96" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="134" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-137" parent="JbCyL7nHgy0AnRVCQc6m-136" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="Role" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-138" parent="JbCyL7nHgy0AnRVCQc6m-136" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="String" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-139" parent="JbCyL7nHgy0AnRVCQc6m-136" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-117" edge="1" parent="1" source="JbCyL7nHgy0AnRVCQc6m-101" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="JbCyL7nHgy0AnRVCQc6m-62">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="840" y="429" />
<mxPoint x="840" y="83" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-118" edge="1" parent="1" source="JbCyL7nHgy0AnRVCQc6m-105" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="JbCyL7nHgy0AnRVCQc6m-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="380" y="455" />
<mxPoint x="380" y="83" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-119" parent="1" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;strokeColor=default;fontSize=16;gradientColor=none;fillColor=default;" value="FamilyThreads" vertex="1">
<mxGeometry height="264" width="320" x="920" y="40" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-120" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="30" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-121" parent="JbCyL7nHgy0AnRVCQc6m-120" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="ID" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-122" parent="JbCyL7nHgy0AnRVCQc6m-120" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigSerial" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-123" parent="JbCyL7nHgy0AnRVCQc6m-120" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="PK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-124" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="56" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-125" parent="JbCyL7nHgy0AnRVCQc6m-124" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="FamilyId" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-126" parent="JbCyL7nHgy0AnRVCQc6m-124" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="BigInt" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-127" parent="JbCyL7nHgy0AnRVCQc6m-124" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="FK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-128" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="320" y="82" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-129" parent="JbCyL7nHgy0AnRVCQc6m-128" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="TelegramTopicID" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-130" parent="JbCyL7nHgy0AnRVCQc6m-128" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="Bigint" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-131" parent="JbCyL7nHgy0AnRVCQc6m-128" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-171" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="108" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-172" parent="JbCyL7nHgy0AnRVCQc6m-171" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="Title" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-173" parent="JbCyL7nHgy0AnRVCQc6m-171" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="String" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-174" parent="JbCyL7nHgy0AnRVCQc6m-171" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-167" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="134" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-168" parent="JbCyL7nHgy0AnRVCQc6m-167" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="Type" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-169" parent="JbCyL7nHgy0AnRVCQc6m-167" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="String" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-170" parent="JbCyL7nHgy0AnRVCQc6m-167" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-132" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="160" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-133" parent="JbCyL7nHgy0AnRVCQc6m-132" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="CreatedAt" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-134" parent="JbCyL7nHgy0AnRVCQc6m-132" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="TimeStamp" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-135" parent="JbCyL7nHgy0AnRVCQc6m-132" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-175" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="186" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-176" parent="JbCyL7nHgy0AnRVCQc6m-175" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="CreatedBy" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-177" parent="JbCyL7nHgy0AnRVCQc6m-175" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="Bigint" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-178" parent="JbCyL7nHgy0AnRVCQc6m-175" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="FK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-183" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="212" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-184" parent="JbCyL7nHgy0AnRVCQc6m-183" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="UpdatedAt" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-185" parent="JbCyL7nHgy0AnRVCQc6m-183" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="TimeStamp" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-186" parent="JbCyL7nHgy0AnRVCQc6m-183" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-179" parent="JbCyL7nHgy0AnRVCQc6m-119" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;strokeColor=inherit;top=0;left=0;bottom=0;right=0;collapsible=0;dropTarget=0;fillColor=none;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontSize=16;" vertex="1">
<mxGeometry height="26" width="320" y="238" as="geometry" />
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-180" parent="JbCyL7nHgy0AnRVCQc6m-179" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;align=left;" value="UpdatedBy" vertex="1">
<mxGeometry height="26" width="160" as="geometry">
<mxRectangle height="26" width="160" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-181" parent="JbCyL7nHgy0AnRVCQc6m-179" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="Bigint" vertex="1">
<mxGeometry height="26" width="120" x="160" as="geometry">
<mxRectangle height="26" width="120" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-182" parent="JbCyL7nHgy0AnRVCQc6m-179" style="shape=partialRectangle;html=1;whiteSpace=wrap;connectable=0;strokeColor=inherit;overflow=hidden;fillColor=none;top=0;left=0;bottom=0;right=0;pointerEvents=1;fontSize=16;" value="FK" vertex="1">
<mxGeometry height="26" width="40" x="280" as="geometry">
<mxRectangle height="26" width="40" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-162" edge="1" parent="1" source="JbCyL7nHgy0AnRVCQc6m-124" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="JbCyL7nHgy0AnRVCQc6m-62">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="880" y="109" />
<mxPoint x="880" y="83" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-187" edge="1" parent="1" source="JbCyL7nHgy0AnRVCQc6m-175" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="JbCyL7nHgy0AnRVCQc6m-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="880" y="239" />
<mxPoint x="880" y="300" />
<mxPoint x="400" y="300" />
<mxPoint x="400" y="83" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-188" edge="1" parent="1" source="JbCyL7nHgy0AnRVCQc6m-179" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="JbCyL7nHgy0AnRVCQc6m-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="900" y="291" />
<mxPoint x="900" y="320" />
<mxPoint x="390" y="320" />
<mxPoint x="390" y="83" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="JbCyL7nHgy0AnRVCQc6m-189" edge="1" parent="1" source="JbCyL7nHgy0AnRVCQc6m-152" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="JbCyL7nHgy0AnRVCQc6m-28">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="410" y="109" />
<mxPoint x="410" y="83" />
</Array>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
+20
View File
@@ -21,16 +21,24 @@ require (
cloud.google.com/go/compute/metadata v0.8.0 // indirect cloud.google.com/go/compute/metadata v0.8.0 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go/vision/v2 v2.9.5 // indirect cloud.google.com/go/vision/v2 v2.9.5 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect
@@ -39,9 +47,11 @@ require (
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -49,8 +59,14 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect github.com/quic-go/quic-go v0.57.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/swaggo/gin-swagger v1.6.1 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
@@ -59,17 +75,21 @@ require (
go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/arch v0.20.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.45.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.18.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.12.0 // indirect golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/api v0.247.0 // indirect google.golang.org/api v0.247.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/grpc v1.74.2 // indirect google.golang.org/grpc v1.74.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
) )
+14
View File
@@ -0,0 +1,14 @@
CREATE TABLE families
(
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
owner_id BIGINT NOT NULL REFERENCES users (id),
telegram_chat_id BIGINT NOT NULL,
telegram_chat_name TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_families_owner_id ON families (owner_id);
CREATE UNIQUE INDEX idx_families_chat_id ON families (telegram_chat_id);
@@ -1 +0,0 @@
DROP TABLE IF EXISTS telegram_chats;
@@ -1,9 +0,0 @@
CREATE TABLE telegram_chats
(
id BIGSERIAL PRIMARY KEY,
telegram_id BIGINT UNIQUE NOT NULL,
title TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_telegram_chats_telegram_id ON telegram_chats (telegram_id);
-12
View File
@@ -1,12 +0,0 @@
CREATE TABLE families
(
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
owner_id BIGINT NOT NULL REFERENCES users (id),
telegram_chat_id BIGINT NOT NULL UNIQUE REFERENCES telegram_chats (id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_families_owner_id ON families (owner_id);
CREATE UNIQUE INDEX idx_families_chat_id ON families (telegram_chat_id);
@@ -0,0 +1,19 @@
CREATE TABLE family_threads
(
id BIGSERIAL PRIMARY KEY,
family_id BIGINT NOT NULL REFERENCES families (id) ON DELETE CASCADE,
type TEXT NOT NULL,
title TEXT NOT NULL,
telegram_topic_id BIGINT NOT NULL,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
created_by BIGINT NOT NULL REFERENCES users (id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (family_id, telegram_topic_id)
);
CREATE UNIQUE INDEX idx_unique_system_threads ON family_threads (family_id, type) WHERE is_system = TRUE;
CREATE INDEX idx_threads_family_id ON family_threads (family_id);
CREATE INDEX idx_threads_family_type ON family_threads (family_id, type);
-27
View File
@@ -1,27 +0,0 @@
CREATE TYPE thread_type AS ENUM (
'expenses',
'movies',
'schedule',
'recipes',
'custom'
);
CREATE TABLE threads
(
id BIGSERIAL PRIMARY KEY,
family_id BIGINT NOT NULL REFERENCES families (id) ON DELETE CASCADE,
type thread_type NOT NULL,
title TEXT NOT NULL,
telegram_topic_id BIGINT NOT NULL,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
created_by BIGINT NOT NULL REFERENCES users (id),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (family_id, telegram_topic_id)
);
CREATE UNIQUE INDEX idx_unique_system_threads ON threads (family_id, type) WHERE is_system = TRUE;
CREATE INDEX idx_threads_family_id ON threads (family_id);
CREATE INDEX idx_threads_family_type ON threads(family_id, type);
+401
View File
@@ -0,0 +1,401 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/users": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Создать пользователя",
"parameters": [
{
"description": "User info",
"name": "user",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.CreateUserRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/users/by-telegram/{telegramId}": {
"get": {
"description": "Возвращает пользователя по его Telegram ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Получить пользователя по Telegram ID",
"parameters": [
{
"type": "integer",
"description": "Telegram ID",
"name": "telegramId",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "invalid telegram id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "user not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/users/{id}": {
"get": {
"description": "Возвращает пользователя по его внутреннему ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Получить пользователя по ID",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "user not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"delete": {
"description": "Удаляет пользователя по его ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Удалить пользователя",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "no content",
"schema": {
"type": "string"
}
},
"400": {
"description": "invalid id",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "user not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"patch": {
"description": "Частично обновляет данные пользователя по ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Обновить пользователя",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Данные для обновления",
"name": "user",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.UpdateUserRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UserResponse"
}
},
"400": {
"description": "invalid id or invalid body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "user not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
"dto.CreateUserRequest": {
"type": "object",
"required": [
"first_name",
"telegram_id"
],
"properties": {
"first_name": {
"type": "string"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"telegram_id": {
"type": "integer"
},
"username": {
"type": "string"
}
}
},
"dto.UpdateUserRequest": {
"type": "object",
"properties": {
"first_name": {
"type": "string"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"dto.UserResponse": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"language_code": {
"type": "string"
},
"last_name": {
"type": "string"
},
"telegram_id": {
"type": "integer"
},
"updated_at": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
+5
View File
@@ -0,0 +1,5 @@
package dto
type ErrorResponse struct {
Message string `json:"message"`
}
@@ -1,10 +1,14 @@
package dto package dto
type HelloRequest struct { import "time"
Name string `form:"name" binding:"required,min=2,max=50"`
}
type AddReceiptRequest struct { type AddReceiptRequest struct {
Number string `json:"number" binding:"required,min=24,max=24"` Number string `json:"number" binding:"required,min=24,max=24"`
Date string `json:"date" binding:"required"` Date string `json:"date" binding:"required"`
} }
type AddReceiptResponse struct {
ID int32 `json:"id"`
Number string `json:"number"`
Date time.Time `json:"date"`
}
-17
View File
@@ -1,17 +0,0 @@
package dto
import "time"
type HelloResponse struct {
Message string `json:"message"`
}
type ErrorResponse struct {
Message string `json:"message"`
}
type AddReceiptResponse struct {
ID int32 `json:"id"`
Number string `json:"number"`
Date time.Time `json:"date"`
}
+43
View File
@@ -0,0 +1,43 @@
package dto
import (
"FamilyHub/src/domain"
"time"
)
type CreateUserRequest struct {
TelegramID int64 `json:"telegram_id" validate:"required"`
Username *string `json:"username"`
FirstName string `json:"first_name" validate:"required"`
LastName *string `json:"last_name"`
LanguageCode *string `json:"language_code"`
}
type UpdateUserRequest struct {
Username *string `json:"username"`
FirstName *string `json:"first_name"`
LastName *string `json:"last_name"`
LanguageCode *string `json:"language_code"`
}
type UserResponse struct {
ID int64 `json:"id"`
TelegramID int64 `json:"telegram_id"`
Username *string `json:"username"`
FirstName string `json:"first_name"`
LastName *string `json:"last_name"`
LanguageCode *string `json:"language_code"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (response *UserResponse) ModelToResponse(u *domain.User) UserResponse {
return UserResponse{
ID: u.ID,
TelegramID: u.TelegramID,
Username: u.Username,
FirstName: u.FirstName,
LastName: u.LastName,
LanguageCode: u.LanguageCode,
CreatedAt: u.CreatedAt.Format(time.RFC3339),
UpdatedAt: u.UpdatedAt.Format(time.RFC3339),
}
}
@@ -1,4 +1,4 @@
package handlers package routers
import ( import (
"FamilyHub/src/api/dto" "FamilyHub/src/api/dto"
@@ -12,15 +12,21 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type ReceiptHandler struct { type ReceiptRouter struct {
service *receiptService.ReceiptService service *receiptService.ReceiptService
} }
func NewReceiptHandler(s *receiptService.ReceiptService) *ReceiptHandler { func NewReceiptRouter(s *receiptService.ReceiptService) *ReceiptRouter {
return &ReceiptHandler{service: s} return &ReceiptRouter{service: s}
}
func (router *ReceiptRouter) RegisterRoutes(r *gin.RouterGroup) {
receipts := r.Group("/receipts")
{
receipts.POST("", router.AddReceipt)
}
} }
func (handler *ReceiptHandler) AddReceipt(context_ *gin.Context) { func (router *ReceiptRouter) AddReceipt(context_ *gin.Context) {
var req dto.AddReceiptRequest var req dto.AddReceiptRequest
if err := context_.ShouldBindJSON(&req); err != nil { if err := context_.ShouldBindJSON(&req); err != nil {
log.Println("bind error:", err) log.Println("bind error:", err)
@@ -36,7 +42,7 @@ func (handler *ReceiptHandler) AddReceipt(context_ *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel() defer cancel()
receipt, err := handler.service.GetReceipt(ctx, isoDate, req.Number) receipt, err := router.service.GetReceipt(ctx, isoDate, req.Number)
if err != nil { if err != nil {
context_.JSON(400, gin.H{"error": err.Error()}) context_.JSON(400, gin.H{"error": err.Error()})
log.Printf("API error, %s", err.Error()) log.Printf("API error, %s", err.Error())
+192
View File
@@ -0,0 +1,192 @@
package routers
import (
"FamilyHub/src/api/dto"
"FamilyHub/src/api/services"
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type UsersRouter struct {
service services.UserService
}
func NewUsersRouter(s *services.UserService) *UsersRouter {
return &UsersRouter{service: *s}
}
func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
users := r.Group("/users")
{
users.POST("", router.CreateUser)
users.GET("/:id", router.GetByID)
users.GET("/by-telegram/:telegramId", router.GetByTelegramID)
users.PATCH("/:id", router.Update)
users.DELETE("/:id", router.Delete)
}
}
// CreateUser GoDoc
// @Summary Создать пользователя
// @Tags Users
// @Accept json
// @Produce json
// @Param user body dto.CreateUserRequest true "User info"
// @Success 201 {object} dto.UserResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /users [post]
func (router *UsersRouter) CreateUser(c *gin.Context) {
var req dto.CreateUserRequest
var resp dto.UserResponse
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := router.service.Create(c.Request.Context(), req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusCreated, resp.ModelToResponse(user))
}
// GetByID GoDoc
// @Summary Получить пользователя по ID
// @Description Возвращает пользователя по его внутреннему ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]string "invalid id"
// @Failure 404 {object} map[string]string "user not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Router /users/{id} [get]
func (router *UsersRouter) GetByID(c *gin.Context) {
var resp dto.UserResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
user, err := router.service.GetByID(c.Request.Context(), id)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, resp.ModelToResponse(user))
}
// GetByTelegramID GoDoc
// @Summary Получить пользователя по Telegram ID
// @Description Возвращает пользователя по его Telegram ID
// @Tags Users
// @Accept json
// @Produce json
// @Param telegramId path int true "Telegram ID"
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]string "invalid telegram id"
// @Failure 404 {object} map[string]string "user not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Router /users/by-telegram/{telegramId} [get]
func (router *UsersRouter) GetByTelegramID(c *gin.Context) {
var resp dto.UserResponse
telegramID, err := strconv.ParseInt(c.Param("telegramId"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid telegram id"})
return
}
user, err := router.service.GetByTelegramID(c.Request.Context(), telegramID)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, resp.ModelToResponse(user))
}
// Update GoDoc
// @Summary Обновить пользователя
// @Description Частично обновляет данные пользователя по ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Param user body dto.UpdateUserRequest true "Данные для обновления"
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]string "invalid id or invalid body"
// @Failure 404 {object} map[string]string "user not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Router /users/{id} [patch]
func (router *UsersRouter) Update(c *gin.Context) {
var resp dto.UserResponse
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req dto.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := router.service.Update(c.Request.Context(), id, req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, resp.ModelToResponse(user))
}
// Delete GoDoc
// @Summary Удалить пользователя
// @Description Удаляет пользователя по его ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path int true "User ID"
// @Success 204 {string} string "no content"
// @Failure 400 {object} map[string]string "invalid id"
// @Failure 404 {object} map[string]string "user not found"
// @Failure 500 {object} map[string]string "internal server error"
// @Router /users/{id} [delete]
func (router *UsersRouter) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := router.service.Delete(c.Request.Context(), id); err != nil {
handleError(c, err)
return
}
c.Status(http.StatusNoContent)
}
func handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, services.ErrUserNotFound):
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
case errors.Is(err, services.ErrInvalidPatch):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
case errors.Is(err, services.ErrTelegramIDMissing):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
}
}
+20 -10
View File
@@ -1,7 +1,9 @@
package api package api
import ( import (
"FamilyHub/src/api/handlers" _ "FamilyHub/src/api/docs"
"FamilyHub/src/api/routers"
"FamilyHub/src/api/services"
"FamilyHub/src/config" "FamilyHub/src/config"
"FamilyHub/src/database" "FamilyHub/src/database"
"FamilyHub/src/integrations/receiptService" "FamilyHub/src/integrations/receiptService"
@@ -11,6 +13,8 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
) )
type Server struct { type Server struct {
@@ -18,7 +22,6 @@ type Server struct {
} }
func NewServer(cfg config.Config) *Server { func NewServer(cfg config.Config) *Server {
handler := gin.Default()
dbManager := &database.Database{ dbManager := &database.Database{
ConnectionString: cfg.DBConnectionString, ConnectionString: cfg.DBConnectionString,
MigrationsPath: "file://migrations", MigrationsPath: "file://migrations",
@@ -27,24 +30,31 @@ func NewServer(cfg config.Config) *Server {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if err := dbManager.RunMigrations(dbConn); err != nil { if err := dbManager.RunMigrations(dbConn); err != nil {
log.Fatal(err) log.Fatal(err)
} }
receiptRepo := repositories.NewReceiptSQLRepository(dbConn) router := gin.Default()
receiptService_ := receiptService.NewReceiptService(receiptRepo)
receiptHandler := handlers.NewReceiptHandler(receiptService_)
if cfg.OpenAPIEnabled { if cfg.OpenAPIEnabled {
handler.GET(cfg.OpenAPIEndpoint) router.GET("/openapi/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
} }
handler.POST("/receipts", receiptHandler.AddReceipt)
apiV1 := router.Group("/api/v1")
receiptRepo := repositories.NewReceiptsSQLRepository(dbConn)
receiptService_ := receiptService.NewReceiptService(receiptRepo)
receiptRouter := routers.NewReceiptRouter(receiptService_)
receiptRouter.RegisterRoutes(apiV1)
usersRepo := repositories.NewUsersSQLRepository(dbConn)
usersService := services.NewUserService(usersRepo)
usersRouter := routers.NewUsersRouter(&usersService)
usersRouter.RegisterRoutes(apiV1)
return &Server{ return &Server{
httpServer: &http.Server{ httpServer: &http.Server{
Addr: cfg.APIHost + ":" + cfg.APIPort, Addr: cfg.APIHost + ":" + cfg.APIPort,
Handler: handler, Handler: router,
}, },
} }
} }
+107
View File
@@ -0,0 +1,107 @@
package services
import (
"FamilyHub/src/api/dto"
"FamilyHub/src/domain"
"FamilyHub/src/repositories"
"context"
"errors"
)
type UserService interface {
Create(ctx context.Context, req dto.CreateUserRequest) (*domain.User, error)
GetByID(ctx context.Context, id int64) (*domain.User, error)
GetByTelegramID(ctx context.Context, telegramID int64) (*domain.User, error)
Update(ctx context.Context, id int64, req dto.UpdateUserRequest) (*domain.User, error)
Delete(ctx context.Context, id int64) error
}
type userService struct {
repo repositories.UsersRepository
}
func NewUserService(repo repositories.UsersRepository) UserService {
return &userService{repo: repo}
}
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidPatch = errors.New("empty update payload")
ErrTelegramIDMissing = errors.New("telegram_id is required")
)
func (s *userService) Create(ctx context.Context, req dto.CreateUserRequest) (*domain.User, error) {
user_ := &domain.User{
TelegramID: req.TelegramID,
Username: req.Username,
FirstName: req.FirstName,
LastName: req.LastName,
LanguageCode: req.LanguageCode,
}
if err := s.repo.Create(ctx, user_); err != nil {
return nil, err
}
return user_, nil
}
func (s *userService) GetByID(ctx context.Context, id int64) (*domain.User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
func (s *userService) GetByTelegramID(ctx context.Context, telegramID int64) (*domain.User, error) {
user, err := s.repo.GetByTelegramID(ctx, telegramID)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
func (s *userService) Update(ctx context.Context, id int64, req dto.UpdateUserRequest) (*domain.User, error) {
existing, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if existing == nil {
return nil, ErrUserNotFound
}
if req.Username == nil &&
req.FirstName == nil &&
req.LastName == nil &&
req.LanguageCode == nil {
return nil, ErrInvalidPatch
}
if err := s.repo.Update(ctx, &domain.User{
ID: id,
Username: req.Username,
FirstName: *req.FirstName,
LastName: req.LastName,
LanguageCode: req.LanguageCode,
}); err != nil {
return nil, err
}
return s.repo.GetByID(ctx, id)
}
func (s *userService) Delete(ctx context.Context, id int64) error {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return err
}
if user == nil {
return ErrUserNotFound
}
return s.repo.Delete(ctx, id)
}
+1 -1
View File
@@ -1,7 +1,7 @@
package config_test package config_test
import ( import (
"GoFinanceManager/internal/config" "FamilyHub/src/config"
"os" "os"
"testing" "testing"
@@ -1,4 +1,4 @@
package models package domain
import "time" import "time"
+16
View File
@@ -0,0 +1,16 @@
package domain
import (
"time"
)
type User struct {
ID int64
TelegramID int64
Username *string
FirstName string
LastName *string
LanguageCode *string
CreatedAt time.Time
UpdatedAt time.Time
}
@@ -1,7 +1,7 @@
package receiptService package receiptService
import ( import (
"FamilyHub/src/domain/models" "FamilyHub/src/domain"
"FamilyHub/src/repositories" "FamilyHub/src/repositories"
"FamilyHub/src/utils" "FamilyHub/src/utils"
"bytes" "bytes"
@@ -18,10 +18,10 @@ import (
type ReceiptService struct { type ReceiptService struct {
client *http.Client client *http.Client
repo repositories.ReceiptRepository repo repositories.ReceiptsRepository
} }
func NewReceiptService(repo repositories.ReceiptRepository) *ReceiptService { func NewReceiptService(repo repositories.ReceiptsRepository) *ReceiptService {
return &ReceiptService{ return &ReceiptService{
client: &http.Client{ client: &http.Client{
Timeout: 60 * time.Second, Timeout: 60 * time.Second,
@@ -39,9 +39,9 @@ func (s *ReceiptService) GetReceipt(
ctx context.Context, ctx context.Context,
date string, date string,
number string, number string,
) (*models.Receipt, error) { ) (*domain.Receipt, error) {
url := "https://ch.info-center.by/ajax/check1.php" url := "https://ch.info-center.by/ajax/check1.php"
var receipt models.Receipt var receipt domain.Receipt
body, contentType := buildMultipartBody(date, number) body, contentType := buildMultipartBody(date, number)
req, err := http.NewRequestWithContext( req, err := http.NewRequestWithContext(
@@ -137,8 +137,8 @@ func buildMultipartBody(date, number string) (*bytes.Buffer, string) {
return body, writer.FormDataContentType() return body, writer.FormDataContentType()
} }
func parsePositions(raw string) ([]models.Position, error) { func parsePositions(raw string) ([]domain.Position, error) {
var positions []models.Position var positions []domain.Position
if raw == "" { if raw == "" {
return positions, nil return positions, nil
-15
View File
@@ -1,15 +0,0 @@
package repositories
import (
"context"
"FamilyHub/src/domain/models"
)
type ReceiptRepository interface {
Create(ctx context.Context, receipt *models.Receipt) (int64, error)
GetByID(ctx context.Context, id int64) (*models.Receipt, error)
GetAll(ctx context.Context, limit, offset int) ([]*models.Receipt, error)
Update(ctx context.Context, receipt *models.Receipt) error
Delete(ctx context.Context, id int64) error
}
@@ -5,18 +5,26 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"FamilyHub/src/domain/models" "FamilyHub/src/domain"
) )
type ReceiptSQLRepository struct { type ReceiptsRepository interface {
Create(ctx context.Context, receipt *domain.Receipt) (int64, error)
GetByID(ctx context.Context, id int64) (*domain.Receipt, error)
GetAll(ctx context.Context, limit, offset int) ([]*domain.Receipt, error)
Update(ctx context.Context, receipt *domain.Receipt) error
Delete(ctx context.Context, id int64) error
}
type ReceiptsSQLRepository struct {
db *sql.DB db *sql.DB
} }
func NewReceiptSQLRepository(db *sql.DB) *ReceiptSQLRepository { func NewReceiptsSQLRepository(db *sql.DB) *ReceiptsSQLRepository {
return &ReceiptSQLRepository{db: db} return &ReceiptsSQLRepository{db: db}
} }
func (r *ReceiptSQLRepository) Create(ctx context.Context, receipt *models.Receipt) (int64, error) { func (r *ReceiptsSQLRepository) Create(ctx context.Context, receipt *domain.Receipt) (int64, error) {
tx, err := r.db.BeginTx(ctx, nil) tx, err := r.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
@@ -129,9 +137,9 @@ func (r *ReceiptSQLRepository) Create(ctx context.Context, receipt *models.Recei
return receiptID, tx.Commit() return receiptID, tx.Commit()
} }
func (r *ReceiptSQLRepository) GetByID(ctx context.Context, id int64) (*models.Receipt, error) { func (r *ReceiptsSQLRepository) GetByID(ctx context.Context, id int64) (*domain.Receipt, error) {
var receipt models.Receipt var receipt domain.Receipt
err := r.db.QueryRowContext(ctx, ` err := r.db.QueryRowContext(ctx, `
SELECT SELECT
@@ -145,7 +153,7 @@ func (r *ReceiptSQLRepository) GetByID(ctx context.Context, id int64) (*models.R
street_to, house_to, street_to, house_to,
kod_soato, oblast_soato, rayon_soato, selsovet_soato, kod_soato, oblast_soato, rayon_soato, selsovet_soato,
doc_num, skno_number, unp, doc_num, skno_number, unp,
success, raw_json success
FROM receipts FROM receipts
WHERE id = ? WHERE id = ?
`, id).Scan( `, id).Scan(
@@ -209,7 +217,7 @@ func (r *ReceiptSQLRepository) GetByID(ctx context.Context, id int64) (*models.R
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var p models.Position var p domain.Position
if err := rows.Scan( if err := rows.Scan(
&p.SectionNumber, &p.SectionNumber,
&p.GTINCode, &p.GTINCode,
@@ -230,7 +238,7 @@ func (r *ReceiptSQLRepository) GetByID(ctx context.Context, id int64) (*models.R
return &receipt, nil return &receipt, nil
} }
func (r *ReceiptSQLRepository) GetAll(ctx context.Context, limit, offset int) ([]*models.Receipt, error) { func (r *ReceiptsSQLRepository) GetAll(ctx context.Context, limit, offset int) ([]*domain.Receipt, error) {
rows, err := r.db.QueryContext(ctx, ` rows, err := r.db.QueryContext(ctx, `
SELECT id, receipt_number, issued_at, total_amount, currency SELECT id, receipt_number, issued_at, total_amount, currency
@@ -243,10 +251,10 @@ func (r *ReceiptSQLRepository) GetAll(ctx context.Context, limit, offset int) ([
} }
defer rows.Close() defer rows.Close()
var receipts []*models.Receipt var receipts []*domain.Receipt
for rows.Next() { for rows.Next() {
var rct models.Receipt var rct domain.Receipt
if err := rows.Scan( if err := rows.Scan(
&rct.ID, &rct.ID,
&rct.ReceiptNumber, &rct.ReceiptNumber,
@@ -262,7 +270,7 @@ func (r *ReceiptSQLRepository) GetAll(ctx context.Context, limit, offset int) ([
return receipts, nil return receipts, nil
} }
func (r *ReceiptSQLRepository) Update(ctx context.Context, receipt *models.Receipt) error { func (r *ReceiptsSQLRepository) Update(ctx context.Context, receipt *domain.Receipt) error {
tx, err := r.db.BeginTx(ctx, nil) tx, err := r.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
@@ -305,7 +313,7 @@ func (r *ReceiptSQLRepository) Update(ctx context.Context, receipt *models.Recei
return tx.Commit() return tx.Commit()
} }
func (r *ReceiptSQLRepository) Delete(ctx context.Context, id int64) error { func (r *ReceiptsSQLRepository) Delete(ctx context.Context, id int64) error {
_, err := r.db.ExecContext(ctx, _, err := r.db.ExecContext(ctx,
`DELETE FROM receipts WHERE id = ?`, `DELETE FROM receipts WHERE id = ?`,
id, id,
+158
View File
@@ -0,0 +1,158 @@
package repositories
import (
"FamilyHub/src/domain"
"context"
"database/sql"
"errors"
)
type UsersRepository interface {
Create(ctx context.Context, user *domain.User) error
GetByID(ctx context.Context, id int64) (*domain.User, error)
GetByTelegramID(ctx context.Context, telegramID int64) (*domain.User, error)
Update(ctx context.Context, user *domain.User) error
Delete(ctx context.Context, id int64) error
}
type UsersSQLRepository struct {
db *sql.DB
}
func NewUsersSQLRepository(db *sql.DB) *UsersSQLRepository {
return &UsersSQLRepository{db: db}
}
func (r *UsersSQLRepository) Create(ctx context.Context, user *domain.User) error {
query := `
INSERT INTO users
(telegram_id, username, first_name, last_name, language_code)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, created_at, updated_at
`
return r.db.QueryRowContext(
ctx,
query,
user.TelegramID,
user.Username,
user.FirstName,
user.LastName,
user.LanguageCode,
).Scan(&user.ID, &user.CreatedAt, &user.UpdatedAt)
}
func (r *UsersSQLRepository) GetByID(ctx context.Context, id int64) (*domain.User, error) {
query := `
SELECT
id,
telegram_id,
username,
first_name,
last_name,
language_code,
created_at,
updated_at
FROM users
WHERE id = $1
`
var user domain.User
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.TelegramID,
&user.Username,
&user.FirstName,
&user.LastName,
&user.LanguageCode,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil // или кастомную ErrNotFound
}
return nil, err
}
return &user, nil
}
func (r *UsersSQLRepository) GetByTelegramID(ctx context.Context, telegramID int64) (*domain.User, error) {
query := `
SELECT
id,
telegram_id,
username,
first_name,
last_name,
language_code,
created_at,
updated_at
FROM users
WHERE telegram_id = $1
`
var user domain.User
err := r.db.QueryRowContext(ctx, query, telegramID).Scan(
&user.ID,
&user.TelegramID,
&user.Username,
&user.FirstName,
&user.LastName,
&user.LanguageCode,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *UsersSQLRepository) Update(ctx context.Context, user *domain.User) error {
query := `
UPDATE users SET
username = $1,
first_name = $2,
last_name = $3,
language_code = $4,
updated_at = now()
WHERE id = $5
RETURNING updated_at
`
return r.db.QueryRowContext(
ctx,
query,
user.Username,
user.FirstName,
user.LastName,
user.LanguageCode,
user.ID,
).Scan(&user.UpdatedAt)
}
func (r *UsersSQLRepository) Delete(ctx context.Context, id int64) error {
query := `DELETE FROM users WHERE id = $1`
result, err := r.db.ExecContext(ctx, query, id)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return sql.ErrNoRows
}
return nil
}