commit da4c45819d97555e6c57d9e4b442dcd15daab543 Author: Mohammed Sohail Date: Mon Feb 5 16:05:28 2024 +0300 init: pilot demo diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..42ff718 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +pb_data \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da48e93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +pb_data/ +bin/ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0da4d6e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1-bookworm as build + +ENV CGO_ENABLED=0 +ENV GOOS=linux +ENV GOARCH=amd64 + +WORKDIR /build + +COPY go.* . +RUN go mod download + +COPY . . +RUN go build -o farmstar-survey-backend -ldflags="-s -w" cmd/farmstar/*.go + +FROM debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +WORKDIR /service + +COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=build /build/farmstar-survey-backend . +COPY LICENSE . +COPY config.toml . + +EXPOSE 8090 + +CMD ["/service/farmstar-survey-backend", "serve", "--http=0.0.0.0:8090"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29ebfa5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/cmd/farmstar/init.go b/cmd/farmstar/init.go new file mode 100644 index 0000000..236ef2c --- /dev/null +++ b/cmd/farmstar/init.go @@ -0,0 +1,160 @@ +package main + +import ( + "context" + "log/slog" + "os" + "strings" + + "github.com/grassrootseconomics/farmstar-survey-backend/internal/worker" + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/custodial" + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/telegram" + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/ussd" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/kamikazechaser/africastalking" + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/plugins/migratecmd" + "github.com/riverqueue/river" + "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivermigrate" +) + +func initLogger() *slog.Logger { + return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelError, + })) +} + +func initConfig() *koanf.Koanf { + var ( + ko = koanf.New(".") + ) + + confFile := file.Provider(confFlag) + if err := ko.Load(confFile, toml.Parser()); err != nil { + lo.Error("could not parse configuration file", err) + os.Exit(1) + } + + if err := ko.Load(env.Provider("FARMSTAR_", ".", func(s string) string { + return strings.ReplaceAll(strings.ToLower( + strings.TrimPrefix(s, "FARMSTAR_")), "__", ".") + }), nil); err != nil { + lo.Error("could not override config from env vars", err) + os.Exit(1) + } + + return ko +} + +func initPocketbase() *pocketbase.PocketBase { + app := pocketbase.New() + + migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{ + Automigrate: true, + }) + + return app +} + +func initPostgres() *pgxpool.Pool { + parsedConfig, err := pgxpool.ParseConfig(ko.MustString("postgres.dsn")) + if err != nil { + lo.Error("could not parse postgres dsn", err) + os.Exit(1) + } + + dbPool, err := pgxpool.NewWithConfig(context.Background(), parsedConfig) + if err != nil { + lo.Error("could not create pgxpool", err) + os.Exit(1) + } + + return dbPool +} + +func initUSSDClient() *ussd.USSDClient { + return ussd.New(ko.MustString("ussd.endpoint")) +} + +func initCustodialClient() *custodial.CustodialClient { + return custodial.New(ko.MustString("custodial.endpoint")) +} + +func initTelegramClient() *telegram.TelegramClient { + return telegram.New(ko.MustString("telegram.key")) +} + +func initATClient() *africastalking.AtClient { + return africastalking.New( + ko.MustString("at.key"), + ko.MustString("at.username"), + false, + ) +} + +func initRiverQueueWWorker() *worker.Worker { + ctx := context.Background() + tx, err := postgresPool.Begin(ctx) + if err != nil { + lo.Error("could not begin pgx tx", err) + os.Exit(1) + } + defer tx.Rollback(ctx) + + migrator := rivermigrate.New(riverpgxv5.New(postgresPool), nil) + _, err = migrator.MigrateTx(context.Background(), tx, rivermigrate.DirectionUp, nil) + if err != nil { + lo.Error("could not migrate river queue", err) + os.Exit(1) + } + + if err := tx.Commit(ctx); err != nil { + lo.Error("could not commit pgx tx", err) + os.Exit(1) + } + + workers := river.NewWorkers() + if err := river.AddWorkerSafely(workers, &worker.RewardsWorker{ + Pocketbase: pocketbaseApp, + USSDClient: ussdClient, + CustodialClient: custodialClient, + VaultAddress: ko.MustString("rewards.vault"), + VoucherAddress: ko.MustString("rewards.voucher"), + }); err != nil { + lo.Error("could not bootstrap rewards worker", err) + os.Exit(1) + } + if err := river.AddWorkerSafely(workers, &worker.SMSWorker{ + AtClient: atClient, + }); err != nil { + lo.Error("could not bootstrap SMS worker", err) + os.Exit(1) + } + if err := river.AddWorkerSafely(workers, &worker.TelegramWorker{ + TgClient: tgClient, + ChatID: ko.MustString("telegram.group"), + }); err != nil { + lo.Error("could not bootstrap tg worker", err) + os.Exit(1) + } + + riverClient, err := river.NewClient(riverpgxv5.New(postgresPool), &river.Config{ + Logger: lo, + Queues: map[string]river.QueueConfig{ + river.QueueDefault: {MaxWorkers: 10}, + }, + Workers: workers, + }) + if err != nil { + lo.Error("could not create a river client", err) + os.Exit(1) + } + + return worker.NewWorker(riverClient) +} diff --git a/cmd/farmstar/main.go b/cmd/farmstar/main.go new file mode 100644 index 0000000..a0071c1 --- /dev/null +++ b/cmd/farmstar/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "flag" + "log/slog" + "os" + + "github.com/grassrootseconomics/farmstar-survey-backend/internal/hooks" + "github.com/grassrootseconomics/farmstar-survey-backend/internal/router" + _ "github.com/grassrootseconomics/farmstar-survey-backend/migrations" + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/custodial" + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/telegram" + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/ussd" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/kamikazechaser/africastalking" + "github.com/knadh/koanf/v2" + "github.com/pocketbase/pocketbase" +) + +var ( + confFlag string + + lo *slog.Logger + ko *koanf.Koanf + + pocketbaseApp *pocketbase.PocketBase + postgresPool *pgxpool.Pool + ussdClient *ussd.USSDClient + custodialClient *custodial.CustodialClient + tgClient *telegram.TelegramClient + atClient *africastalking.AtClient +) + +func init() { + flag.StringVar(&confFlag, "config", "config.toml", "Config file location") + flag.Parse() + + lo = initLogger() + ko = initConfig() +} + +func main() { + pocketbaseApp = initPocketbase() + postgresPool = initPostgres() + + ussdClient = initUSSDClient() + custodialClient = initCustodialClient() + tgClient = initTelegramClient() + atClient = initATClient() + + worker := initRiverQueueWWorker() + if err := worker.Start(); err != nil { + lo.Error("could not start worker", err) + os.Exit(1) + } + + hooks := hooks.NewHooks(hooks.Opts{ + PB: pocketbaseApp, + Worker: worker, + RedemptionVault: ko.MustString("rewards.number"), + }) + hooks.Bootsrap() + + router := router.NewRouter(router.Opts{ + PB: pocketbaseApp, + USSDClient: ussdClient, + }) + router.Bootsrap() + + if err := pocketbaseApp.Start(); err != nil { + lo.Error("could not start pocketbase ", err) + os.Exit(1) + } +} diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml new file mode 100644 index 0000000..7412235 --- /dev/null +++ b/dev/docker-compose.yaml @@ -0,0 +1,22 @@ +version: "3.9" +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + user: postgres + environment: + - POSTGRES_PASSWORD=postgres + - POSTGRES_USER=postgres + volumes: + - farmstar-pg:/var/lib/postgresql/data + - ./init_db.sql:/docker-entrypoint-initdb.d/init_db.sql + ports: + - "127.0.0.1:5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 10s + timeout: 5s + retries: 5 +volumes: + farmstar-pg: + driver: local diff --git a/dev/init_db.sql b/dev/init_db.sql new file mode 100644 index 0000000..a3b2c5f --- /dev/null +++ b/dev/init_db.sql @@ -0,0 +1 @@ +CREATE DATABASE farmstar_worker; \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..694bc9c --- /dev/null +++ b/go.mod @@ -0,0 +1,113 @@ +module github.com/grassrootseconomics/farmstar-survey-backend + +go 1.21.6 + +require ( + github.com/jackc/pgx/v5 v5.5.1 + github.com/knadh/koanf/parsers/toml v0.1.0 + github.com/knadh/koanf/providers/env v0.1.0 + github.com/knadh/koanf/providers/file v0.1.0 + github.com/knadh/koanf/v2 v2.0.1 + github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 + github.com/pocketbase/dbx v1.10.1 + github.com/pocketbase/pocketbase v0.19.4 + github.com/riverqueue/river v0.0.16 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.0.16 +) + +require ( + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go v1.46.6 // indirect + github.com/aws/aws-sdk-go-v2 v1.21.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/config v1.19.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.92 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // indirect + github.com/aws/smithy-go v1.15.0 // indirect + github.com/carlmjohnson/requests v0.23.5 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + github.com/domodwyer/mailyak/v3 v3.6.2 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/ganigeorgiev/fexpr v0.3.0 // indirect + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/google/wire v0.5.0 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/kamikazechaser/africastalking v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/riverqueue/river/riverdriver v0.0.15 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + go.opencensus.io v0.24.0 // indirect + gocloud.dev v0.34.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect + golang.org/x/image v0.13.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.16.1 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + google.golang.org/api v0.148.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + lukechampine.com/uint128 v1.3.0 // indirect + modernc.org/cc/v3 v3.41.0 // indirect + modernc.org/ccgo/v3 v3.16.15 // indirect + modernc.org/libc v1.28.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/sqlite v1.26.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eadad58 --- /dev/null +++ b/go.sum @@ -0,0 +1,420 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= +cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y= +cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= +cloud.google.com/go/storage v1.31.0 h1:+S3LjjEN2zZ+L5hOwj4+1OkGCsLVe0NzpXKQ1pSdTCI= +cloud.google.com/go/storage v1.31.0/go.mod h1:81ams1PrhW16L4kF7qg+4mTq7SRs5HsbDTM0bWvrwJ0= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.46.6 h1:6wFnNC9hETIZLMf6SOTN7IcclrOGwp/n9SLp8Pjt6E8= +github.com/aws/aws-sdk-go v1.46.6/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= +github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14 h1:Sc82v7tDQ/vdU1WtuSyzZ1I7y/68j//HJ6uozND1IDs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14/go.mod h1:9NCTOURS8OpxvoAVHq79LK81/zC78hfRWFn+aL0SPcY= +github.com/aws/aws-sdk-go-v2/config v1.19.1 h1:oe3vqcGftyk40icfLymhhhNysAwk0NfiwkDi2GTPMXs= +github.com/aws/aws-sdk-go-v2/config v1.19.1/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.92 h1:nLA7dGFC6v4P6b+hzqt5GqIGmIuN+jTJzojfdOLXWFE= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.92/go.mod h1:h+ei9z19AhoN+Dac92DwkzfbJ4mFUea92xgl5pKSG0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6 h1:wmGLw2i8ZTlHLw7a9ULGfQbuccw8uIiNr6sol5bFzc8= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6/go.mod h1:Q0Hq2X/NuL7z8b1Dww8rmOFl+jzusKEcyvkKspwdpyc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15 h1:7R8uRYyXzdD71KWVCL78lJZltah6VVznXBazvKjfH58= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15/go.mod h1:26SQUPcTNgV1Tapwdt4a1rOsYRsnBsJHLMPoxK2b0d8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38 h1:skaFGzv+3kA+v2BPKhuekeb1Hbb105+44r8ASC+q5SE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38/go.mod h1:epIZoRSSbRIwLPJU5F+OldHhwZPBdpDeQkRdCeY3+00= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6 h1:9ulSU5ClouoPIYhDQdg9tpl83d5Yb91PXTKK+17q+ow= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6/go.mod h1:lnc2taBsR9nTlz9meD+lhFZZ9EWY712QHrRflWpTcOA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2 h1:Ll5/YVCOzRB+gxPqs2uD0R7/MyATC0w85626glSKmp4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2/go.mod h1:Zjfqt7KhQK+PO1bbOsFNzKgaq7TcxzmEoDWN8lM0qzQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8= +github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/carlmjohnson/requests v0.23.5 h1:NPANcAofwwSuC6SIMwlgmHry2V3pLrSqRiSBKYbNHHA= +github.com/carlmjohnson/requests v0.23.5/go.mod h1:zG9P28thdRnN61aD7iECFhH5iGGKX2jIjKQD9kqYH+o= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/domodwyer/mailyak/v3 v3.6.2 h1:x3tGMsyFhTCaxp6ycgR0FE/bu5QiNp+hetUuCOBXMn8= +github.com/domodwyer/mailyak/v3 v3.6.2/go.mod h1:lOm/u9CyCVWHeaAmHIdF4RiKVxKUT/H5XX10lIKAL6c= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/ganigeorgiev/fexpr v0.3.0 h1:RwSyJBME+g/XdzlUW0paH/4VXhLHPg+rErtLeC7K8Ew= +github.com/ganigeorgiev/fexpr v0.3.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= +github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= +github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= +github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk= +github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8 h1:gpptm606MZYGaMHMsB4Srmb6EbW/IVHnt04rcMXnkBQ= +github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= +github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= +github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kamikazechaser/africastalking v1.0.0 h1:UI5BXgj87o5lJrrlUikY86qCSvt97CJgTETPo/u3osU= +github.com/kamikazechaser/africastalking v1.0.0/go.mod h1:hgKqb7Zl080ay0BzCBI25iqnoK2ac1tWYPfquXODGwc= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= +github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= +github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= +github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= +github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g= +github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4= +github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pocketbase/dbx v1.10.1 h1:cw+vsyfCJD8YObOVeqb93YErnlxwYMkNZ4rwN0G0AaA= +github.com/pocketbase/dbx v1.10.1/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs= +github.com/pocketbase/pocketbase v0.19.4 h1:PtgbrNMg2wZqI4BJqnvnc/RtDOYan+qcQyJqf2diLp4= +github.com/pocketbase/pocketbase v0.19.4/go.mod h1:P6efmT5amltbiSLbdG42D+yPAkKv0Jg449k6HHyAu5w= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riverqueue/river v0.0.16 h1:rqbWGyiGMLD2OkzzPHhyR1Oh0l80ZPavXLH8+cerTIs= +github.com/riverqueue/river v0.0.16/go.mod h1:1zLtldMgoTfDj4ouXFNGsF4bzqb9Y2Ds3WuDFanugJ0= +github.com/riverqueue/river/riverdriver v0.0.15 h1:BB26eGIB+xK4dpQ9w5WUxWHbDZbk0E+tmajGRYvI/hM= +github.com/riverqueue/river/riverdriver v0.0.15/go.mod h1:vtgL7tRTSB6rzeVEDppehd/rPx3Is+WBYb17Zj0+KsE= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.0.15 h1:vBS22g1I3gaSRnYnk9yrvn+oTk0odVTmJw1pIDAFD5w= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.0.15/go.mod h1:ERxJyW2g+1oBzTn5MRfSWi6Z83I5Dumj9J+E4rCe2kc= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.0.16 h1:kUr40A6sVrw3blSxQQo3T0LgiO6qRbSzK2u4Vjml6Ww= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.0.16/go.mod h1:w365SHh6QB96Yea/SsGBdUAhGGvlWhU9+v2AVwJSjBc= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +gocloud.dev v0.34.0 h1:LzlQY+4l2cMtuNfwT2ht4+fiXwWf/NmPTnXUlLmGif4= +gocloud.dev v0.34.0/go.mod h1:psKOachbnvY3DAOPbsFVmLIErwsbWPUG2H5i65D38vE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= +golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.148.0 h1:HBq4TZlN4/1pNcu0geJZ/Q50vIwIXT532UIMYoo0vOs= +google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a h1:fwgW9j3vHirt4ObdHoYNwuO24BEZjSzbh+zPaNWoiY8= +google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:EMfReVxb80Dq1hhioy0sOsY9jCE46YDgHlJ7fWVUWRE= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= +lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0= +modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.28.0 h1:kHB6LtDBV8DEAK7aZT1vWvP92abW9fb8cjb1P9UTpUE= +modernc.org/libc v1.28.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw= +modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= diff --git a/internal/hooks/distributor.go b/internal/hooks/distributor.go new file mode 100644 index 0000000..26c3b1a --- /dev/null +++ b/internal/hooks/distributor.go @@ -0,0 +1,40 @@ +package hooks + +import ( + "fmt" + + "github.com/grassrootseconomics/farmstar-survey-backend/internal/worker" + "github.com/pocketbase/pocketbase/core" +) + +func (r *HooksContainer) bootstrapDistributorHook() { + r.pb.OnModelAfterCreate("distributor_base").Add(func(e *core.ModelEvent) error { + farmerRecord, err := r.pb.Dao().FindRecordById("distributor_base", e.Model.GetId()) + if err != nil { + return err + } + + userRecord, err := r.pb.Dao().FindRecordById("users", farmerRecord.GetString("user")) + if err != nil { + return err + } + + phone := userRecord.GetString("phone") + + msg := fmt.Sprintf( + "New distributors's survey completed:\n\nname: %s\nphone: %s", + userRecord.GetString("name"), + userRecord.GetString("phone"), + ) + + if err := r.worker.QueueSMSTask(phone, worker.DistributorComplete, ""); err != nil { + return err + } + + if err := r.worker.QueueTgTask(msg); err != nil { + return err + } + + return nil + }) +} diff --git a/internal/hooks/farmer.go b/internal/hooks/farmer.go new file mode 100644 index 0000000..e5137a7 --- /dev/null +++ b/internal/hooks/farmer.go @@ -0,0 +1,44 @@ +package hooks + +import ( + "fmt" + + "github.com/grassrootseconomics/farmstar-survey-backend/internal/worker" + "github.com/pocketbase/pocketbase/core" +) + +func (r *HooksContainer) bootstrapFarmerHook() { + r.pb.OnModelAfterCreate("farmer_farm").Add(func(e *core.ModelEvent) error { + farmerRecord, err := r.pb.Dao().FindRecordById("farmer_farm", e.Model.GetId()) + if err != nil { + return err + } + + userRecord, err := r.pb.Dao().FindRecordById("users", farmerRecord.GetString("user")) + if err != nil { + return err + } + + phone := userRecord.GetString("phone") + + msg := fmt.Sprintf( + "New farmer's survey completed:\n\nname: %s\nphone: %s", + userRecord.GetString("name"), + phone, + ) + + if err := r.worker.QueueSMSTask(phone, worker.FarmerComplete, ""); err != nil { + return err + } + + if err := r.worker.QueueTgTask(msg); err != nil { + return err + } + + if err := r.worker.QueueRewardsTask(phone, 130_000_000); err != nil { + return err + } + + return nil + }) +} diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go new file mode 100644 index 0000000..d7a324c --- /dev/null +++ b/internal/hooks/hooks.go @@ -0,0 +1,36 @@ +package hooks + +import ( + "github.com/grassrootseconomics/farmstar-survey-backend/internal/worker" + "github.com/pocketbase/pocketbase" +) + +type ( + Opts struct { + PB *pocketbase.PocketBase + Worker *worker.Worker + RedemptionVault string + } + + HooksContainer struct { + pb *pocketbase.PocketBase + worker *worker.Worker + redemptionVault string + } +) + +func NewHooks(o Opts) *HooksContainer { + return &HooksContainer{ + pb: o.PB, + worker: o.Worker, + redemptionVault: o.RedemptionVault, + } +} + +func (r *HooksContainer) Bootsrap() { + r.bootstrapRegistrationHook() + r.bootstrapFarmerHook() + r.bootstrapDistributorHook() + r.bootstrapTransactionHook() + r.bootstrapRedeemHook() +} diff --git a/internal/hooks/redeem.go b/internal/hooks/redeem.go new file mode 100644 index 0000000..7f42b4e --- /dev/null +++ b/internal/hooks/redeem.go @@ -0,0 +1,71 @@ +package hooks + +import ( + "fmt" + + "github.com/pocketbase/pocketbase/core" +) + +var ( + distributors = map[string]string{ + "maraba_investments": "Maraba Investments", + "farmers_center": "Farmers Center", + "farmers_world": "Farmers World", + "farmers_desk": "Farmers Desk", + "mazao_na_afya": "Mazao na Afya", + } + + rewardsSchedule = map[int]string{ + 60: "25% discount on a 50kg bag of EverGrow", + 120: "50% discount on a 50kg bag of EverGrow", + 180: "75% discount on a 50kg bag of EverGrow", + 240: "1 free 50kg bag of EverGrow", + } +) + +func (r *HooksContainer) bootstrapRedeemHook() { + r.pb.OnModelAfterCreate("redemption").Add(func(e *core.ModelEvent) error { + redemptionRecord, err := r.pb.Dao().FindRecordById("redemption", e.Model.GetId()) + if err != nil { + return err + } + + initiatorRecord, err := r.pb.Dao().FindRecordById("users", redemptionRecord.GetString("distributor_initiator")) + if err != nil { + return err + } + + counterpartyRecord, err := r.pb.Dao().FindRecordById("users", redemptionRecord.GetString("farmer")) + if err != nil { + return err + } + + tgMsg := fmt.Sprintf( + "New redemption request:\n\ndistributor name: %s\ndistributor entity: %s\nfarmer name: %s\nredemption request: %d FSP", + initiatorRecord.GetString("name"), + redemptionRecord.GetString("distributor"), + counterpartyRecord.GetString("name"), + redemptionRecord.GetInt("fsp_redemption_request"), + ) + + if err := r.worker.QueueTgTask(tgMsg); err != nil { + return err + } + + if err := r.worker.QueueSMSTask( + counterpartyRecord.GetString("phone"), + "", + fmt.Sprintf( + "FSP redemption request received at %s for %s. Send %d FSP to %s to complete redemption.", + distributors[redemptionRecord.GetString("distributor")], + rewardsSchedule[redemptionRecord.GetInt("fsp_redemption_request")], + redemptionRecord.GetInt("fsp_redemption_request"), + r.redemptionVault, + ), + ); err != nil { + return err + } + + return nil + }) +} diff --git a/internal/hooks/registration.go b/internal/hooks/registration.go new file mode 100644 index 0000000..d433353 --- /dev/null +++ b/internal/hooks/registration.go @@ -0,0 +1,43 @@ +package hooks + +import ( + "fmt" + + "github.com/grassrootseconomics/farmstar-survey-backend/internal/worker" + "github.com/pocketbase/pocketbase/core" +) + +func (r *HooksContainer) bootstrapRegistrationHook() { + r.pb.OnModelAfterCreate("users").Add(func(e *core.ModelEvent) error { + userRecord, err := r.pb.Dao().FindRecordById("users", e.Model.GetId()) + if err != nil { + return err + } + + participantType := userRecord.GetString("participant_type") + participantPhone := userRecord.GetString("phone") + + tgMsg := fmt.Sprintf( + "New %s signup:\n\nname: %s\nphone: %s", + participantType, + userRecord.GetString("name"), + participantPhone, + ) + + if err := r.worker.QueueTgTask(tgMsg); err != nil { + return err + } + + if participantType == "farmer" { + if err := r.worker.QueueSMSTask(participantPhone, worker.FarmerRegistration, ""); err != nil { + return err + } + } else { + if err := r.worker.QueueSMSTask(participantPhone, worker.DistributorRegistration, ""); err != nil { + return err + } + } + + return nil + }) +} diff --git a/internal/hooks/transaction.go b/internal/hooks/transaction.go new file mode 100644 index 0000000..95e8c5b --- /dev/null +++ b/internal/hooks/transaction.go @@ -0,0 +1,93 @@ +package hooks + +import ( + "database/sql" + "fmt" + "log" + + "github.com/grassrootseconomics/farmstar-survey-backend/internal/worker" + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" +) + +func (r *HooksContainer) bootstrapTransactionHook() { + r.pb.OnModelAfterCreate("transactions").Add(func(e *core.ModelEvent) error { + log.Println("hook?") + transactionRecord, err := r.pb.Dao().FindRecordById("transactions", e.Model.GetId()) + if err != nil { + return err + } + + quantity := uint(transactionRecord.GetInt("evergrow_quantity")) + + initiatorRecord, err := r.pb.Dao().FindRecordById("users", transactionRecord.GetString("initiator")) + if err != nil { + return err + } + + counterpartyRecord, err := r.pb.Dao().FindRecordById("users", transactionRecord.GetString("counterparty")) + if err != nil { + return err + } + + inverseTransactionRecord, err := r.pb.Dao().FindFirstRecordByFilter( + "transactions", + "completed = {:completed} && evergrow_quantity = {:quantity} && initiator = {:initiator} && counterparty = {:counterparty}", + dbx.Params{ + "completed": false, + "quantity": transactionRecord.GetInt("evergrow_quantity"), + "initiator": counterpartyRecord.GetId(), + "counterparty": initiatorRecord.GetId(), + }, + ) + if err != nil && err != sql.ErrNoRows { + return err + } + + if inverseTransactionRecord != nil { + if isFarmer(initiatorRecord.GetString("participant_type")) { + if err := r.worker.QueueRewardsTask(initiatorRecord.GetString("phone"), 65_000_000*quantity); err != nil { + return err + } + } + + if isFarmer(counterpartyRecord.GetString("participant_type")) { + if err := r.worker.QueueRewardsTask(counterpartyRecord.GetString("phone"), 65_000_000*quantity); err != nil { + return err + } + } + + transactionRecord.Set("completed", true) + if err := r.pb.Dao().SaveRecord(transactionRecord); err != nil { + return err + } + + inverseTransactionRecord.Set("completed", true) + if err := r.pb.Dao().SaveRecord(inverseTransactionRecord); err != nil { + return err + } + } + + if err := r.worker.QueueSMSTask(initiatorRecord.GetString("phone"), worker.PurchaseComplete, ""); err != nil { + return err + } + + msg := fmt.Sprintf( + "New purchase survey completed:\n\ninitiator: %s\ncounterparty: %s\nquantity: %d\nfeedback: %s", + fmt.Sprintf("%s (%s)", initiatorRecord.GetString("name"), initiatorRecord.GetString("participant_type")), + fmt.Sprintf("%s (%s)", counterpartyRecord.GetString("name"), counterpartyRecord.GetString("participant_type")), + transactionRecord.GetInt("evergrow_quantity"), + transactionRecord.GetString("feedback"), + ) + + if err := r.worker.QueueTgTask(msg); err != nil { + return err + } + + return nil + }) +} + +func isFarmer(value string) bool { + return value == "farmer" +} diff --git a/internal/router/distributor.go b/internal/router/distributor.go new file mode 100644 index 0000000..8a4a20f --- /dev/null +++ b/internal/router/distributor.go @@ -0,0 +1,101 @@ +package router + +import ( + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" +) + +func (r *RouterContainer) bootstrapDistributorRoute() { + r.PB.OnBeforeServe().Add(func(e *core.ServeEvent) error { + e.Router.POST("/distributor", func(c echo.Context) error { + requestData := struct { + Phone string `json:"phone"` + Distributor string `json:"distributor_name"` + FertilizerType string `json:"type"` + SyntheticPercentage string `json:"synthetic_percentage"` + OrganicPercentage string `json:"organic_percentage"` + SyntheticFactors []string `json:"synthetic_factors"` + OrganicFactors []string `json:"organic_factors"` + Trends string `json:"trends"` + Obstacles string `json:"obstacles"` + NeedEducation string `json:"need_education"` + FarmerStrategies string `json:"farmer_strategies"` + FarmstarStrategies string `json:"farmstar_strategies"` + }{} + if err := c.Bind(&requestData); err != nil { + return apis.NewBadRequestError("Failed to read request data", err) + } + + if err := r.PB.Dao().RunInTransaction(func(txDao *daos.Dao) error { + userRecord, err := r.PB.Dao().FindFirstRecordByData("users", "phone", requestData.Phone) + if err != nil { + return err + } + + if userRecord.GetString("participant_type") != "distributor" { + return apis.NewNotFoundError("User is not registered as a distributor", err) + } + + distributorExt, err := r.PB.Dao().FindCollectionByNameOrId("distributor_both_fertilizers") + if err != nil { + return err + } + + distributorExtRecord := models.NewRecord(distributorExt) + distributorExtForm := forms.NewRecordUpsert(r.PB, distributorExtRecord) + distributorExtForm.SetDao(txDao) + + distributorExtForm.LoadData(map[string]any{ + "user": userRecord.Id, + "synthetic_percentage": requestData.SyntheticPercentage, + "organic_percentage": requestData.OrganicPercentage, + "synthetic_factors": requestData.SyntheticFactors, + "organic_factors": requestData.OrganicFactors, + "trends": requestData.Trends, + "obstacles": requestData.Obstacles, + "need_education": requestData.NeedEducation, + "farmer_strategies": requestData.FarmerStrategies, + }) + + if err := distributorExtForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit distributor details", err) + } + + distributorBaseCollection, err := r.PB.Dao().FindCollectionByNameOrId("distributor_base") + if err != nil { + return err + } + + distributorBaseRecord := models.NewRecord(distributorBaseCollection) + distributorBaseForm := forms.NewRecordUpsert(r.PB, distributorBaseRecord) + distributorBaseForm.SetDao(txDao) + + distributorBaseForm.LoadData(map[string]any{ + "user": userRecord.Id, + "distributor_name": requestData.Distributor, + "fertilizer_type": requestData.FertilizerType, + }) + + if err := distributorBaseForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit additional distributor details", err) + } + + return nil + }); err != nil { + c.JSON(400, map[string]any{"ok": "false", "error": err.Error()}) + + return nil + } + + c.JSON(200, map[string]any{"ok": "true"}) + + return nil + }) + + return nil + }) +} diff --git a/internal/router/farmer.go b/internal/router/farmer.go new file mode 100644 index 0000000..5d44c2f --- /dev/null +++ b/internal/router/farmer.go @@ -0,0 +1,213 @@ +package router + +import ( + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" +) + +func (r *RouterContainer) bootstrapFarmerSurveyRoute() { + r.PB.OnBeforeServe().Add(func(e *core.ServeEvent) error { + e.Router.POST("/farmer", func(c echo.Context) error { + requestData := struct { + Phone string `json:"phone"` + County string `json:"county" ` + SubCounty string `json:"sub_county"` + FarmingAreaAcres string `json:"farming_area_acres"` + PlannedCrops []string `json:"planned_crops"` + PastHarvestDetails []struct { + CropType string `json:"crop_type"` + AverageHarvest string `json:"average_harvest"` + AverageEarning string `json:"average_earning"` + } `json:"past_harvest_details"` + TotalExpenditure string `json:"total_expenditure"` + SeedsExpenditurePercentage string `json:"seeds_expenditure_percentage"` + FertilizerExpenditurePercentage string `json:"fertilizer_expenditure_percentage"` + CropsProtectionExpenditurePercentage string `json:"crops_protection_expenditure_percentage"` + SyntheticFertilizersExpenditurePercentage string `json:"synthetic_fertilizers_expenditure_percentage"` + NaturalFertilizersExpenditure string `json:"natural_fertilizers_expenditure_percentage"` + IncreasedExpenses []struct { + ExpenseType string `json:"expense_type"` + IncreasedExpenseActions []string `json:"increased_expense_actions"` + ExpenseActionOverallEffect string `json:"expense_action_overall_effect"` + } `json:"increased_expenses"` + EvergrowPast string `json:"evergrow_past"` + EvergrowFirst string `json:"evergrow_first"` + EvergrowApplication string `json:"evergrow_application"` + EvergrowCrops []string `json:"evergrow_crops"` + EvergrowYield string `json:"evergrow_yield"` + OtherFertilizers string `json:"other_fertilizers"` + PurchaseChannels []string `json:"purchase_channels"` + OtherFertilizersDetails []struct { + OtherFertilizerType string `json:"other_fertilizer_type"` + OtherFertilizerApplication string `json:"other_fertilizer_application"` + OtherFertilizerCrops []string `json:"other_fertilizer_crops"` + } `json:"other_fertilizers_details"` + }{} + if err := c.Bind(&requestData); err != nil { + return apis.NewBadRequestError("Failed to read request data", err) + } + + if err := r.PB.Dao().RunInTransaction(func(txDao *daos.Dao) error { + userRecord, err := r.PB.Dao().FindFirstRecordByData("users", "phone", requestData.Phone) + if err != nil { + return apis.NewNotFoundError("Phone number not found", err) + } + + if userRecord.GetString("participant_type") != "farmer" { + return apis.NewNotFoundError("User is not registered as a farmer", err) + } + + farmerFarmCollection, err := r.PB.Dao().FindCollectionByNameOrId("farmer_farm") + if err != nil { + return err + } + + farmerFarmRecord := models.NewRecord(farmerFarmCollection) + farmerFarmForm := forms.NewRecordUpsert(r.PB, farmerFarmRecord) + farmerFarmForm.SetDao(txDao) + + farmerFarmForm.LoadData(map[string]any{ + "user": userRecord.Id, + "county": requestData.County, + "sub_county": requestData.SubCounty, + "farming_area_acres": requestData.FarmingAreaAcres, + "planned_crops": requestData.PlannedCrops, + }) + + if err := farmerFarmForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit farm details", err) + } + + farmerPastHarvestCollection, err := r.PB.Dao().FindCollectionByNameOrId("farmer_past_harvest") + if err != nil { + return err + } + + for _, v := range requestData.PastHarvestDetails { + farmerPastHarvestRecord := models.NewRecord(farmerPastHarvestCollection) + farmerPastHarvestForm := forms.NewRecordUpsert(r.PB, farmerPastHarvestRecord) + farmerPastHarvestForm.SetDao(txDao) + + farmerPastHarvestForm.LoadData(map[string]any{ + "user": userRecord.Id, + "crop": v.CropType, + "average_harvest": v.AverageHarvest, + "average_earning": v.AverageEarning, + }) + + if err := farmerPastHarvestForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit harvest details", err) + } + } + + farmerExpenditureBaseCollection, err := r.PB.Dao().FindCollectionByNameOrId("farmer_expenditure_base") + if err != nil { + return err + } + + farmerExpenditureBaseRecord := models.NewRecord(farmerExpenditureBaseCollection) + farmerExpenditureBaseFarmForm := forms.NewRecordUpsert(r.PB, farmerExpenditureBaseRecord) + farmerExpenditureBaseFarmForm.SetDao(txDao) + + farmerExpenditureBaseFarmForm.LoadData(map[string]any{ + "user": userRecord.Id, + "total_expenditure": requestData.TotalExpenditure, + "seeds_expenditure_percentage": requestData.SeedsExpenditurePercentage, + "fertilizer_expenditure_percentage": requestData.FertilizerExpenditurePercentage, + "crops_protection_expenditure_percentage": requestData.CropsProtectionExpenditurePercentage, + "synthetic_fertilizers_expenditure_percentage": requestData.SyntheticFertilizersExpenditurePercentage, + "natural_fertilizers_expenditure_percentage": requestData.NaturalFertilizersExpenditure, + }) + + if err := farmerExpenditureBaseFarmForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit expenditure base details", err) + } + + farmerExpenditureIncreasedCollection, err := r.PB.Dao().FindCollectionByNameOrId("farmer_expenditure_increased") + if err != nil { + return err + } + + for _, v := range requestData.IncreasedExpenses { + farmerExpenditureIncreasedRecord := models.NewRecord(farmerExpenditureIncreasedCollection) + farmerExpenditureIncreasedForm := forms.NewRecordUpsert(r.PB, farmerExpenditureIncreasedRecord) + farmerExpenditureIncreasedForm.SetDao(txDao) + + farmerExpenditureIncreasedForm.LoadData(map[string]any{ + "user": userRecord.Id, + "expense": v.ExpenseType, + "actions": v.IncreasedExpenseActions, + "overall_effect": v.ExpenseActionOverallEffect, + }) + + if err := farmerExpenditureIncreasedForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit farmer expenditure increased details", err) + } + } + + farmerEvergrowCollection, err := r.PB.Dao().FindCollectionByNameOrId("farmer_evergrow") + if err != nil { + return err + } + + farmerEvergrowRecord := models.NewRecord(farmerEvergrowCollection) + farmerEvergrowForm := forms.NewRecordUpsert(r.PB, farmerEvergrowRecord) + farmerEvergrowForm.SetDao(txDao) + + farmerEvergrowForm.LoadData(map[string]any{ + "user": userRecord.Id, + "evergrow_past": requestData.EvergrowPast, + "evergrow_first": requestData.EvergrowFirst, + "evergrow_application": requestData.EvergrowApplication, + "evergrow_crops": requestData.EvergrowCrops, + "evergrow_yield": requestData.EvergrowYield, + "other_fertilizers": requestData.OtherFertilizers, + "purchase_channels": requestData.PurchaseChannels, + }) + + if err := farmerEvergrowForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit evergrow details", err) + } + + farmerOtherFertilizersCollection, err := r.PB.Dao().FindCollectionByNameOrId("farmer_other_fertilizers") + if err != nil { + return err + } + + for _, v := range requestData.OtherFertilizersDetails { + farmerOtherFertilizersRecord := models.NewRecord(farmerOtherFertilizersCollection) + farmerOtherFertilizersForm := forms.NewRecordUpsert(r.PB, farmerOtherFertilizersRecord) + farmerOtherFertilizersForm.SetDao(txDao) + + farmerOtherFertilizersForm.LoadData(map[string]any{ + "user": userRecord.Id, + "fertilizer_type": v.OtherFertilizerType, + "fertilizer_application": v.OtherFertilizerApplication, + "crops": v.OtherFertilizerCrops, + }) + + if err := farmerOtherFertilizersForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit other fertilizers details", err) + } + } + + return nil + + }); err != nil { + c.JSON(400, map[string]any{"ok": "false", "error": err.Error()}) + + return nil + } + + c.JSON(200, map[string]any{"ok": "true"}) + + return nil + }) + + return nil + }) +} diff --git a/internal/router/redeem.go b/internal/router/redeem.go new file mode 100644 index 0000000..3a74a90 --- /dev/null +++ b/internal/router/redeem.go @@ -0,0 +1,79 @@ +package router + +import ( + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/daos" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" +) + +func (r *RouterContainer) bootstrapRedeemRoute() { + r.PB.OnBeforeServe().Add(func(e *core.ServeEvent) error { + e.Router.POST("/redeem", func(c echo.Context) error { + requestData := struct { + DistributorPhone string `json:"distributor_phone"` + Distributor string `json:"distributor"` + FarmerPhone string `json:"farmer_phone"` + Redemption string `json:"redemption_options"` + }{} + if err := c.Bind(&requestData); err != nil { + return apis.NewBadRequestError("Failed to read request data", err) + } + + if err := r.PB.Dao().RunInTransaction(func(txDao *daos.Dao) error { + distributorRecord, err := r.PB.Dao().FindFirstRecordByData("users", "phone", requestData.DistributorPhone) + if err != nil { + return err + } + + if distributorRecord.GetString("participant_type") != "distributor" { + return apis.NewNotFoundError("User is not registered as a distributor", err) + } + + farmerRecord, err := r.PB.Dao().FindFirstRecordByData("users", "phone", requestData.FarmerPhone) + if err != nil { + return err + } + + if farmerRecord.GetString("participant_type") != "farmer" { + return apis.NewNotFoundError("User is not registered as a farmer", err) + } + + redemptionCollection, err := r.PB.Dao().FindCollectionByNameOrId("redemption") + if err != nil { + return err + } + + redemptionRecord := models.NewRecord(redemptionCollection) + redemptionForm := forms.NewRecordUpsert(r.PB, redemptionRecord) + redemptionForm.SetDao(txDao) + + redemptionForm.LoadData(map[string]any{ + "distributor_initiator": distributorRecord.Id, + "distributor": requestData.Distributor, + "farmer": farmerRecord.Id, + "fsp_redemption_request": requestData.Redemption, + }) + + if err := redemptionForm.Submit(); err != nil { + return apis.NewBadRequestError("Failed to submit distributor details", err) + } + + return nil + + }); err != nil { + c.JSON(400, map[string]any{"ok": "false", "error": err.Error()}) + + return nil + } + + c.JSON(200, map[string]any{"ok": "true"}) + + return nil + }) + + return nil + }) +} diff --git a/internal/router/registration.go b/internal/router/registration.go new file mode 100644 index 0000000..8c470a3 --- /dev/null +++ b/internal/router/registration.go @@ -0,0 +1,49 @@ +package router + +import ( + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" +) + +func (r *RouterContainer) bootstrapRegistrationRoute() { + r.PB.OnBeforeServe().Add(func(e *core.ServeEvent) error { + e.Router.POST("/registration", func(c echo.Context) error { + data := apis.RequestInfo(c).Data + phone := data["phone"].(string) + + address, err := r.ussd.GetAddress(c.Request().Context(), phone) + if err != nil { + return err + } + + if address == "" { + return apis.NewNotFoundError("Phone # not registered on Sarafu Network", nil) + } + + collection, err := r.PB.Dao().FindCollectionByNameOrId("users") + if err != nil { + return err + } + + record := models.NewRecord(collection) + form := forms.NewRecordUpsert(r.PB, record) + + if err := form.LoadRequest(c.Request(), ""); err != nil { + return apis.NewBadRequestError("Failed to register", err) + } + + if err := form.Submit(); err != nil { + return apis.NewBadRequestError("Failed to register", err) + } + + c.JSON(200, map[string]any{"ok": "true"}) + + return nil + }) + + return nil + }) +} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..193599b --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,33 @@ +package router + +import ( + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/ussd" + "github.com/pocketbase/pocketbase" +) + +type ( + Opts struct { + PB *pocketbase.PocketBase + USSDClient *ussd.USSDClient + } + + RouterContainer struct { + PB *pocketbase.PocketBase + ussd *ussd.USSDClient + } +) + +func NewRouter(o Opts) *RouterContainer { + return &RouterContainer{ + PB: o.PB, + ussd: o.USSDClient, + } +} + +func (r *RouterContainer) Bootsrap() { + r.bootstrapRegistrationRoute() + r.bootstrapFarmerSurveyRoute() + r.bootstrapTransactionRoute() + r.bootstrapDistributorRoute() + r.bootstrapRedeemRoute() +} diff --git a/internal/router/transaction.go b/internal/router/transaction.go new file mode 100644 index 0000000..25aeadc --- /dev/null +++ b/internal/router/transaction.go @@ -0,0 +1,100 @@ +package router + +import ( + "log" + + "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/apis" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" +) + +func (r *RouterContainer) bootstrapTransactionRoute() { + r.PB.OnBeforeServe().Add(func(e *core.ServeEvent) error { + e.Router.POST("/transaction", func(c echo.Context) error { + requestData := struct { + Phone string `json:"phone"` + Tx string `json:"tx" ` + TxBuyType string `json:"buy"` + TxSellType string `json:"sell"` + TxBuyDate string `json:"buy_date"` + TxSellDate string `json:"sell_date"` + Feedback string `json:"feedback"` + EvergrowQuantityPurchase string `json:"evergrow_quantity_purchase"` + EvergrowQuantitySale string `json:"evergrow_quantity_sale"` + DistributorName string `json:"distributor_name"` + }{} + if err := c.Bind(&requestData); err != nil { + return apis.NewBadRequestError("Failed to read request data", err) + } + + initiatorRecord, err := r.PB.Dao().FindFirstRecordByData("users", "phone", requestData.Phone) + if err != nil { + return apis.NewNotFoundError("Initiator not registered", err) + } + + txCollection, err := r.PB.Dao().FindCollectionByNameOrId("transactions") + if err != nil { + return err + } + + txRecord := models.NewRecord(txCollection) + txForm := forms.NewRecordUpsert(r.PB, txRecord) + + txData := map[string]any{ + "initiator": initiatorRecord.Id, + "tx_type": requestData.Tx, + "feedback": requestData.Feedback, + "completed": false, + } + + if requestData.Tx == "buy" { + log.Println("buy") + counterpartyRecord, err := r.PB.Dao().FindFirstRecordByData("users", "phone", requestData.TxBuyType) + if err != nil { + return apis.NewNotFoundError("Counterparty not registered", err) + } + + if counterpartyRecord.GetString("participant_type") != "distributor" { + return apis.NewNotFoundError("Counterparty is not registered as a distributor", err) + } + + txData["evergrow_quantity"] = requestData.EvergrowQuantityPurchase + txData["distributor_name"] = requestData.DistributorName + txData["counterparty"] = counterpartyRecord.Id + txData["tx_date"] = requestData.TxBuyDate + } + + if requestData.Tx == "sell" { + log.Println("sell") + counterpartyRecord, err := r.PB.Dao().FindFirstRecordByData("users", "phone", requestData.TxSellType) + if err != nil { + return apis.NewNotFoundError("", err) + } + + if counterpartyRecord.GetString("participant_type") != "farmer" { + return apis.NewNotFoundError("Counterparty is not registered as a farmer", err) + } + + txData["evergrow_quantity"] = requestData.EvergrowQuantitySale + txData["counterparty"] = counterpartyRecord.Id + txData["tx_date"] = requestData.TxSellDate + } + + txForm.LoadData(txData) + + log.Println("pre_submit") + if err := txForm.Submit(); err != nil { + log.Println(err) + return apis.NewBadRequestError("Failed to submit tx details", err) + } + + c.JSON(200, map[string]any{"ok": "true"}) + + return nil + }) + + return nil + }) +} diff --git a/internal/worker/rewards.go b/internal/worker/rewards.go new file mode 100644 index 0000000..318fc17 --- /dev/null +++ b/internal/worker/rewards.go @@ -0,0 +1,79 @@ +package worker + +import ( + "context" + "errors" + + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/custodial" + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/ussd" + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/forms" + "github.com/pocketbase/pocketbase/models" + "github.com/riverqueue/river" +) + +type ( + RewardsArgs struct { + Phone string `json:"phone"` + Amount uint `json:"amount"` + } + + RewardsWorker struct { + river.WorkerDefaults[RewardsArgs] + + Pocketbase *pocketbase.PocketBase + USSDClient *ussd.USSDClient + CustodialClient *custodial.CustodialClient + VaultAddress string + VoucherAddress string + } +) + +func (RewardsArgs) Kind() string { + return "rewards" +} + +func (w *RewardsWorker) Work(ctx context.Context, job *river.Job[RewardsArgs]) error { + userRecord, err := w.Pocketbase.Dao().FindFirstRecordByData("users", "phone", job.Args.Phone) + if err != nil { + return err + } + + rewardsCollection, err := w.Pocketbase.Dao().FindCollectionByNameOrId("rewards") + if err != nil { + return err + } + rewardsRecord := models.NewRecord(rewardsCollection) + rewardsForm := forms.NewRecordUpsert(w.Pocketbase, rewardsRecord) + + address, err := w.USSDClient.GetAddress(ctx, job.Args.Phone) + if err != nil { + return err + } + + if address == "" { + return errors.New("ussd account not found") + } + + trackingId, err := w.CustodialClient.SignTransfer(ctx, custodial.SignTransferPayload{ + Amount: job.Args.Amount, + To: address, + From: w.VaultAddress, + VoucherAddress: w.VoucherAddress, + }) + if err != nil { + return err + } + + rewardsForm.LoadData(map[string]any{ + "receiver": userRecord.Id, + "ge_tracking_id": trackingId, + "value": job.Args.Amount / 1_000_000, + }) + + if err := rewardsForm.Submit(); err != nil { + return err + } + + return nil +} diff --git a/internal/worker/sms.go b/internal/worker/sms.go new file mode 100644 index 0000000..fe54a81 --- /dev/null +++ b/internal/worker/sms.go @@ -0,0 +1,56 @@ +package worker + +import ( + "context" + + "github.com/kamikazechaser/africastalking" + "github.com/riverqueue/river" +) + +type ( + SMSType string + SMSArgs struct { + SMS SMSType `json:"SMS"` + Phone string `json:"phone"` + Text string `json:"text"` + } + + SMSWorker struct { + river.WorkerDefaults[SMSArgs] + + AtClient *africastalking.AtClient + } +) + +const ( + FarmerRegistration SMSType = "You have registered as a farmer for the FarmStar rewards program. Follow the link https://t.ly/Vin2T to complete your registration and receive FSP." + DistributorRegistration SMSType = "You have registered as a distributor for the FarmStar rewards program. Follow the link https://t.ly/U7SbY to complete your registration and receive FSP." + FarmerComplete SMSType = "Thank you for completing the farmer survey, rewards will be sent soon. Use the link https://t.ly/2CxGt to record purchases of EverGrow Organic Fertilizer." + DistributorComplete SMSType = "Thank you for completing the distributor survey. Use the link https://t.ly/2CxGt to record purchases of EverGrow Organic Fertilizer." + PurchaseComplete SMSType = "Thank you for completing the purchase survey." +) + +func (SMSArgs) Kind() string { + return "sms" +} + +func (w *SMSWorker) Work(ctx context.Context, job *river.Job[SMSArgs]) error { + var text string + + if job.Args.Text != "" { + text = job.Args.Text + } else { + text = string(job.Args.SMS) + } + + _, err := w.AtClient.SendBulkSMS(ctx, africastalking.BulkSMSInput{ + To: []string{"+254" + job.Args.Phone[1:]}, + From: "Sarafu", + Message: text, + }) + if err != nil { + return err + } + + return nil +} diff --git a/internal/worker/telegram.go b/internal/worker/telegram.go new file mode 100644 index 0000000..6dfcb5a --- /dev/null +++ b/internal/worker/telegram.go @@ -0,0 +1,39 @@ +package worker + +import ( + "context" + + "github.com/grassrootseconomics/farmstar-survey-backend/pkg/telegram" + "github.com/riverqueue/river" +) + +type TgMessage string + +type ( + TelegramArgs struct { + Msg string `json:"msg"` + } + + TelegramWorker struct { + river.WorkerDefaults[TelegramArgs] + + TgClient *telegram.TelegramClient + ChatID string + } +) + +func (TelegramArgs) Kind() string { + return "telegram" +} + +func (w *TelegramWorker) Work(ctx context.Context, job *river.Job[TelegramArgs]) error { + _, err := w.TgClient.SendMessage(ctx, telegram.MessageInput{ + ChatID: w.ChatID, + Text: job.Args.Msg, + }) + if err != nil { + return err + } + + return nil +} diff --git a/internal/worker/worker.go b/internal/worker/worker.go new file mode 100644 index 0000000..9875d89 --- /dev/null +++ b/internal/worker/worker.go @@ -0,0 +1,68 @@ +package worker + +import ( + "context" + "time" + + "github.com/jackc/pgx/v5" + "github.com/riverqueue/river" +) + +type ( + Worker struct { + Client *river.Client[pgx.Tx] + } +) + +func NewWorker(riverClient *river.Client[pgx.Tx]) *Worker { + return &Worker{ + Client: riverClient, + } +} + +func (w *Worker) Start() error { + return w.Client.Start(context.Background()) +} + +func (w *Worker) Stop() error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + return w.Client.Stop(ctx) +} + +func (w *Worker) QueueRewardsTask(phone string, amount uint) error { + _, err := w.Client.Insert(context.Background(), RewardsArgs{ + Phone: phone, + Amount: amount, + }, nil) + if err != nil { + return err + } + + return nil +} + +func (w *Worker) QueueSMSTask(phone string, SMS SMSType, text string) error { + _, err := w.Client.Insert(context.Background(), SMSArgs{ + Phone: phone, + SMS: SMS, + Text: text, + }, nil) + if err != nil { + return err + } + + return nil +} + +func (w *Worker) QueueTgTask(msg string) error { + _, err := w.Client.Insert(context.Background(), TelegramArgs{ + Msg: msg, + }, nil) + if err != nil { + return err + } + + return nil +} diff --git a/migrations/1707137988_collections_snapshot.go b/migrations/1707137988_collections_snapshot.go new file mode 100644 index 0000000..fd14c3a --- /dev/null +++ b/migrations/1707137988_collections_snapshot.go @@ -0,0 +1,1319 @@ +package migrations + +import ( + "encoding/json" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" + m "github.com/pocketbase/pocketbase/migrations" + "github.com/pocketbase/pocketbase/models" +) + +func init() { + m.Register(func(db dbx.Builder) error { + jsonData := `[ + { + "id": "no89qd9ku8qo11e", + "created": "2023-12-18 09:15:06.545Z", + "updated": "2024-01-31 11:54:55.087Z", + "name": "users", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "rbi2ukrm", + "name": "name", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 2, + "max": 150, + "pattern": "" + } + }, + { + "system": false, + "id": "4l7bnhvh", + "name": "phone", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 10, + "max": 10, + "pattern": "^(07|01)(\\d){8}$" + } + }, + { + "system": false, + "id": "tvicnnyp", + "name": "gender", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "female", + "male", + "transgender", + "other", + "no_response" + ] + } + }, + { + "system": false, + "id": "otfoqjhn", + "name": "age_group", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "20-29", + "30-39", + "40-49", + "50-59", + "60-69", + "70+" + ] + } + }, + { + "system": false, + "id": "mf34mr31", + "name": "participant_type", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "farmer", + "distributor" + ] + } + }, + { + "system": false, + "id": "to7xfaj9", + "name": "activated", + "type": "bool", + "required": false, + "presentable": false, + "unique": false, + "options": {} + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_ljmcgDQ` + "`" + ` ON ` + "`" + `users` + "`" + ` (` + "`" + `phone` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "uutt9wj7d5ejmhz", + "created": "2024-01-02 09:20:31.919Z", + "updated": "2024-01-31 11:54:55.132Z", + "name": "farmer_farm", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "z2puetdy", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "murxdavm", + "name": "county", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "kirinyaga", + "muranga", + "nakuru", + "meru", + "uasin_gishu", + "kajiado" + ] + } + }, + { + "system": false, + "id": "zj3ufuxl", + "name": "sub_county", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 48, + "pattern": "" + } + }, + { + "system": false, + "id": "8kouirkv", + "name": "farming_area_acres", + "type": "number", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 1, + "max": null, + "noDecimal": false + } + }, + { + "system": false, + "id": "p4n5v8yf", + "name": "planned_crops", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 12, + "values": [ + "rice", + "coffee", + "tea", + "sugarcane", + "miraa", + "avocados", + "maize", + "potatoes", + "sorghum", + "other_fruits", + "other_vegetables", + "other_grains" + ] + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_AFv2Mj9` + "`" + ` ON ` + "`" + `farmer_farm` + "`" + ` (` + "`" + `user` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "5pbnsptw25ip6b1", + "created": "2024-01-02 10:05:21.762Z", + "updated": "2024-01-31 11:54:55.133Z", + "name": "farmer_past_harvest", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "xhv4eaig", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "gt06xc9v", + "name": "crop", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "rice", + "coffee", + "tea", + "sugarcane", + "miraa", + "avocados", + "maize", + "potatoes", + "sorghum", + "other_fruits", + "other_vegetables", + "other_grains" + ] + } + }, + { + "system": false, + "id": "1d5brrhe", + "name": "average_harvest", + "type": "number", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 1, + "max": null, + "noDecimal": false + } + }, + { + "system": false, + "id": "ppm9bhbp", + "name": "average_earning", + "type": "number", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 1, + "max": null, + "noDecimal": false + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_YIwO9th` + "`" + ` ON ` + "`" + `farmer_past_harvest` + "`" + ` (\n ` + "`" + `user` + "`" + `,\n ` + "`" + `crop` + "`" + `\n)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "5ntmsa3wexlrqvb", + "created": "2024-01-02 10:37:18.392Z", + "updated": "2024-01-31 11:54:55.134Z", + "name": "farmer_expenditure_base", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "v4mgvwjl", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "epub5bri", + "name": "total_expenditure", + "type": "number", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 1, + "max": null, + "noDecimal": false + } + }, + { + "system": false, + "id": "zxysfvfy", + "name": "seeds_expenditure_percentage", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 100, + "noDecimal": true + } + }, + { + "system": false, + "id": "fgplvyyp", + "name": "fertilizer_expenditure_percentage", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 100, + "noDecimal": true + } + }, + { + "system": false, + "id": "twefdlqh", + "name": "crops_protection_expenditure_percentage", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 100, + "noDecimal": true + } + }, + { + "system": false, + "id": "vwuti9fs", + "name": "synthetic_fertilizers_expenditure_percentage", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 100, + "noDecimal": true + } + }, + { + "system": false, + "id": "jeumwxyx", + "name": "natural_fertilizers_expenditure_percentage", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 100, + "noDecimal": true + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_pEBpN7N` + "`" + ` ON ` + "`" + `farmer_expenditure_base` + "`" + ` (` + "`" + `user` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "vlcx36spbdrxxe2", + "created": "2024-01-02 15:21:44.736Z", + "updated": "2024-01-31 11:54:55.126Z", + "name": "farmer_expenditure_increased", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "b1hhoaxw", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "yrvfoc0e", + "name": "expense", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "seeds", + "fertilizers", + "crop_protection" + ] + } + }, + { + "system": false, + "id": "pe41jgz8", + "name": "actions", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 5, + "values": [ + "no_change", + "cheap_alternative", + "less_quantity", + "more_quantity", + "other" + ] + } + }, + { + "system": false, + "id": "16wscle0", + "name": "overall_effect", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "increased_yields", + "no_change_yields", + "decreased_yields", + "other" + ] + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_wwcFC9l` + "`" + ` ON ` + "`" + `farmer_expenditure_increased` + "`" + ` (\n ` + "`" + `user` + "`" + `,\n ` + "`" + `expense` + "`" + `\n)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "4f0a5b2wec6okxi", + "created": "2024-01-02 15:40:45.994Z", + "updated": "2024-01-31 11:54:55.138Z", + "name": "farmer_evergrow", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "r7hegvf3", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "ymkvcxmw", + "name": "evergrow_past", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "yes", + "no" + ] + } + }, + { + "system": false, + "id": "maxzlwxk", + "name": "evergrow_first", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": 2018, + "max": null, + "noDecimal": true + } + }, + { + "system": false, + "id": "silbtgao", + "name": "evergrow_application", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": 1, + "max": null, + "noDecimal": false + } + }, + { + "system": false, + "id": "5kxslfpx", + "name": "evergrow_crops", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 12, + "values": [ + "rice", + "coffee", + "tea", + "sugarcane", + "miraa", + "avocados", + "maize", + "potatoes", + "sorghum", + "other_fruits", + "other_vegetables", + "other_grains" + ] + } + }, + { + "system": false, + "id": "yvcfbmgq", + "name": "evergrow_yield", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": 0, + "max": 100, + "noDecimal": false + } + }, + { + "system": false, + "id": "kfjmtcd8", + "name": "other_fertilizers", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "yes", + "no", + "not_sure" + ] + } + }, + { + "system": false, + "id": "fl4s3aca", + "name": "purchase_channels", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 5, + "values": [ + "manufacturers", + "distributors", + "resellers", + "farmers", + "other" + ] + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_q5i3j4r` + "`" + ` ON ` + "`" + `farmer_evergrow` + "`" + ` (` + "`" + `user` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "o7i7dkfc0x2bw22", + "created": "2024-01-02 16:07:23.643Z", + "updated": "2024-01-31 11:54:55.137Z", + "name": "farmer_other_fertilizers", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "vxiny0jx", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "xswrk51v", + "name": "fertilizer_type", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "synthetic", + "commercial_organic", + "self_made" + ] + } + }, + { + "system": false, + "id": "njshkmtb", + "name": "fertilizer_application", + "type": "number", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": 1, + "max": null, + "noDecimal": false + } + }, + { + "system": false, + "id": "ltb2rowr", + "name": "crops", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 12, + "values": [ + "rice", + "coffee", + "tea", + "sugarcane", + "miraa", + "avocados", + "maize", + "potatoes", + "sorghum", + "other_fruits", + "other_vegetables", + "other_grains" + ] + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_yRpihp0` + "`" + ` ON ` + "`" + `farmer_other_fertilizers` + "`" + ` (\n ` + "`" + `user` + "`" + `,\n ` + "`" + `fertilizer_type` + "`" + `\n)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "w7vsnhq55laqara", + "created": "2024-01-05 08:01:27.594Z", + "updated": "2024-01-31 15:28:56.809Z", + "name": "transactions", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "s54vsu9e", + "name": "initiator", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "novr3jwr", + "name": "tx_type", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "buy", + "sell" + ] + } + }, + { + "system": false, + "id": "rhzeviz3", + "name": "tx_date", + "type": "date", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": "", + "max": "" + } + }, + { + "system": false, + "id": "fhrpguaa", + "name": "feedback", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "mp9n48r0", + "name": "evergrow_quantity", + "type": "number", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "noDecimal": false + } + }, + { + "system": false, + "id": "ssxqbyea", + "name": "counterparty", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "rjvhxlwa", + "name": "distributor_name", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "maraba_investments", + "farmers_center", + "farmers_world", + "farmers_desk", + "mazao_na_afya" + ] + } + }, + { + "system": false, + "id": "kwzoawdf", + "name": "completed", + "type": "bool", + "required": false, + "presentable": false, + "unique": false, + "options": {} + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_tf57wcj` + "`" + ` ON ` + "`" + `transactions` + "`" + ` (\n ` + "`" + `initiator` + "`" + `,\n ` + "`" + `tx_type` + "`" + `,\n ` + "`" + `tx_date` + "`" + `,\n ` + "`" + `evergrow_quantity` + "`" + `,\n ` + "`" + `counterparty` + "`" + `,\n ` + "`" + `distributor_name` + "`" + `,\n ` + "`" + `completed` + "`" + `\n)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "hcgj54gqdujejom", + "created": "2024-01-07 08:46:55.596Z", + "updated": "2024-01-31 11:54:55.139Z", + "name": "distributor_base", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "hni6qzrl", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "zadtcvlf", + "name": "distributor_name", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "maraba_investments", + "farmers_center", + "farmers_world", + "farmers_desk", + "mazao_na_afya" + ] + } + }, + { + "system": false, + "id": "sbchpt8f", + "name": "fertilizer_type", + "type": "select", + "required": true, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "synthetic", + "organic", + "both" + ] + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_HM1joZe` + "`" + ` ON ` + "`" + `distributor_base` + "`" + ` (` + "`" + `user` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "7c6woqfe2ds77z7", + "created": "2024-01-07 08:53:59.056Z", + "updated": "2024-01-31 11:54:55.145Z", + "name": "distributor_both_fertilizers", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "4lfninvo", + "name": "user", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": true, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "1jqo9v3q", + "name": "synthetic_percentage", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": 0, + "max": 100, + "noDecimal": false + } + }, + { + "system": false, + "id": "st7q1h1q", + "name": "organic_percentage", + "type": "number", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": 0, + "max": 100, + "noDecimal": false + } + }, + { + "system": false, + "id": "j2lja34g", + "name": "synthetic_factors", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 7, + "values": [ + "increased_crop_yield", + "cost_effective", + "ease_of_application", + "availability", + "marketing", + "govt_incentives", + "other" + ] + } + }, + { + "system": false, + "id": "hizudbmp", + "name": "organic_factors", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 10, + "values": [ + "increased_crop_yield", + "cost_effective", + "ease_of_application", + "availability", + "marketing", + "govt_incentives", + "other", + "environmental_concerns", + "soil_health", + "consumer_demand" + ] + } + }, + { + "system": false, + "id": "dwlhi6wp", + "name": "trends", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 250, + "pattern": "" + } + }, + { + "system": false, + "id": "wddxsdtk", + "name": "obstacles", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 250, + "pattern": "" + } + }, + { + "system": false, + "id": "q5flyusx", + "name": "need_education", + "type": "select", + "required": false, + "presentable": false, + "unique": false, + "options": { + "maxSelect": 1, + "values": [ + "yes", + "no" + ] + } + }, + { + "system": false, + "id": "j32zm5eo", + "name": "organic_strategies", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 250, + "pattern": "" + } + }, + { + "system": false, + "id": "joxlvz5k", + "name": "farmstar_strategies", + "type": "text", + "required": false, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": 500, + "pattern": "" + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_xLt1fh6` + "`" + ` ON ` + "`" + `distributor_both_fertilizers` + "`" + ` (` + "`" + `user` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "x7rkbev8ny0nte4", + "created": "2024-02-01 10:43:44.058Z", + "updated": "2024-02-01 10:50:27.630Z", + "name": "rewards", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "zuszdgix", + "name": "receiver", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "dmrczxxp", + "name": "ge_tracking_id", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "obhsiosq", + "name": "value", + "type": "number", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "noDecimal": true + } + } + ], + "indexes": [ + "CREATE UNIQUE INDEX ` + "`" + `idx_mG3rro1` + "`" + ` ON ` + "`" + `rewards` + "`" + ` (` + "`" + `ge_tracking_id` + "`" + `)" + ], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + }, + { + "id": "ukijuvvvum7pkuz", + "created": "2024-02-01 14:14:28.912Z", + "updated": "2024-02-01 14:14:28.912Z", + "name": "redemption", + "type": "base", + "system": false, + "schema": [ + { + "system": false, + "id": "nnqddcbc", + "name": "distributor_initiator", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "96bbrwxz", + "name": "distributor", + "type": "text", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "pattern": "" + } + }, + { + "system": false, + "id": "rr1lvsdt", + "name": "farmer", + "type": "relation", + "required": true, + "presentable": false, + "unique": false, + "options": { + "collectionId": "no89qd9ku8qo11e", + "cascadeDelete": false, + "minSelect": null, + "maxSelect": 1, + "displayFields": null + } + }, + { + "system": false, + "id": "nk1xzoo6", + "name": "fsp_redemption_request", + "type": "number", + "required": true, + "presentable": false, + "unique": false, + "options": { + "min": null, + "max": null, + "noDecimal": true + } + } + ], + "indexes": [], + "listRule": null, + "viewRule": null, + "createRule": null, + "updateRule": null, + "deleteRule": null, + "options": {} + } + ]` + + collections := []*models.Collection{} + if err := json.Unmarshal([]byte(jsonData), &collections); err != nil { + return err + } + + return daos.New(db).ImportCollections(collections, true, nil) + }, func(db dbx.Builder) error { + return nil + }) +} diff --git a/pkg/custodial/custodial.go b/pkg/custodial/custodial.go new file mode 100644 index 0000000..35ea2d5 --- /dev/null +++ b/pkg/custodial/custodial.go @@ -0,0 +1,47 @@ +package custodial + +import ( + "context" + + "github.com/carlmjohnson/requests" +) + +type ( + CustodialClient struct { + endpoint string + } + + SignTransferPayload struct { + Amount uint `json:"amount"` + From string `json:"from"` + To string `json:"to"` + VoucherAddress string `json:"voucherAddress"` + } + + CustodialResponse struct { + Ok bool `json:"ok"` + Result struct { + TrackingId string `json:"trackingId"` + } `json:"result"` + } +) + +func New(endpoint string) *CustodialClient { + return &CustodialClient{ + endpoint: endpoint, + } +} + +func (cc *CustodialClient) SignTransfer(ctx context.Context, payload SignTransferPayload) (string, error) { + var resp CustodialResponse + + if err := requests. + URL(cc.endpoint). + BodyJSON(&payload). + ToJSON(&resp). + Fetch(ctx); err != nil { + return "", err + } + + return resp.Result.TrackingId, nil +} diff --git a/pkg/telegram/telegram.go b/pkg/telegram/telegram.go new file mode 100644 index 0000000..6ca35c3 --- /dev/null +++ b/pkg/telegram/telegram.go @@ -0,0 +1,52 @@ +package telegram + +import ( + "context" + "errors" + + "github.com/carlmjohnson/requests" +) + +const ( + endpoint = "https://api.telegram.org" +) + +type ( + TelegramClient struct { + endpoint string + chatID string + } + + MessageInput struct { + ChatID string `json:"chat_id"` + Text string `json:"text"` + } + + TgResponse struct { + Ok bool `json:"ok"` + } +) + +func New(apiKey string) *TelegramClient { + return &TelegramClient{ + endpoint: endpoint + "/bot" + apiKey + "/", + } +} + +func (tg *TelegramClient) SendMessage(ctx context.Context, payload MessageInput) (bool, error) { + var resp TgResponse + + if err := requests. + URL(tg.endpoint + "sendMessage"). + BodyJSON(&payload). + ToJSON(&resp). + Fetch(ctx); err != nil { + return false, err + } + + if !resp.Ok { + return false, errors.New("telegram error") + } + + return true, nil +} diff --git a/pkg/ussd/ussd.go b/pkg/ussd/ussd.go new file mode 100644 index 0000000..f67d7c0 --- /dev/null +++ b/pkg/ussd/ussd.go @@ -0,0 +1,36 @@ +package ussd + +import ( + "context" + + "github.com/carlmjohnson/requests" +) + +type ( + USSDClient struct { + endpoint string + } + + USSDResponse struct { + Address string `json:"address"` + } +) + +func New(endpoint string) *USSDClient { + return &USSDClient{ + endpoint: endpoint, + } +} + +func (uc *USSDClient) GetAddress(ctx context.Context, phone string) (string, error) { + var resp USSDResponse + + if err := requests. + URL(uc.endpoint + phone). + ToJSON(&resp). + Fetch(ctx); err != nil { + return "", err + } + + return resp.Address, nil +}